# ------------------------------------------------------------------------------
# Name: graph/primitives.py
# Purpose: Classes for graphing in matplotlib and/or other graphing tools.
#
# Authors: Christopher Ariza
# Michael Scott Asato Cuthbert
# Evan Lynch
#
# Copyright: Copyright © 2009-2023 Michael Scott Asato Cuthbert,
# License: BSD, see license.txt
# ------------------------------------------------------------------------------
'''
Object definitions for graphing and plotting :class:`~music21.stream.Stream` objects.
The :class:`~music21.graph.primitives.Graph` object subclasses primitive,
abstract fundamental graphing archetypes using the matplotlib library.
From the highest level to the lowest level of usage, ways of graphing are as follows:
1. streamObj.plot('graphName')
2. graph.plot.Class(streamObj).run()
3. plotter = graph.primitives.Class(); plotter.data = ...; plotter.process()
4. Use matplotlib directly to create your graph.
'''
from __future__ import annotations
import math
import pathlib
import random
import typing as t
import unittest
if t.TYPE_CHECKING:
import matplotlib.axes
import matplotlib.figure
from music21 import common
from music21.converter.subConverters import SubConverter
from music21 import environment
from music21.graph.utilities import (getExtendedModules,
GraphException,
getColor,
accidentalLabelToUnicode,
)
from music21 import prebase
environLocal = environment.Environment('graph.primitives')
# ------------------------------------------------------------------------------
[docs]
class Graph(prebase.ProtoM21Object):
'''
A music21.graph.primitives.Graph is an object that represents a visual graph or
plot, automating the creation and configuration of this graph in matplotlib.
It is a low-level object that most music21 users do not need to call directly;
yet, as most graphs will take keyword arguments that specify the
look of graphs, they are important to know about.
The keyword arguments can be provided for configuration are:
* doneAction (see below)
* alpha (which describes how transparent elements of the graph are)
* dpi
* colorBackgroundData
* colorBackgroundFigure
* colorGrid,
* title (a string)
* figureSize (a tuple of two ints)
* colors (a list of colors to cycle through)
* tickFontSize
* tickColors (a dict of 'x': '#color', 'y': '#color')
* titleFontSize
* labelFontSize
* fontFamily
* hideXGrid
* hideYGrid
* xTickLabelRotation
* marker
* markersize
Graph objects do not manipulate Streams or other music21 data; they only
manipulate raw data formatted for each Graph subclass, hence it is
unlikely that users will call this class directly.
The `doneAction` argument determines what happens after the graph
has been processed. Currently, there are three options, 'write' creates
a file on disk (this is the default), while 'show' opens an
interactive GUI browser. The
third option, None, does the processing but does not write any output.
figureSize:
A two-element iterable.
Scales all graph components but because of matplotlib limitations
(esp. on 3d graphs) no all labels scale properly.
defaults to .figureSizeDefault
>>> a = graph.primitives.Graph(title='a graph of some data to be given soon', tickFontSize=9)
>>> a.data = [[0, 2], [1, 3]]
>>> a.graphType
'genericGraph'
'''
graphType = 'genericGraph'
axisKeys: tuple[str, ...] = ('x', 'y')
figureSizeDefault: tuple[int|float, ...] = (6, 6)
keywordConfigurables: tuple[str, ...] = (
'alpha',
'colorBackgroundData',
'colorBackgroundFigure',
'colorGrid',
'colors',
'doneAction',
'dpi',
'figureSize',
'fontFamily',
'hideXGrid',
'hideYGrid',
'labelFontSize',
'marker',
'markersize',
'tickColors',
'tickFontSize',
'title',
'titleFontSize',
'xTickLabelHorizontalAlignment',
'xTickLabelRotation',
'xTickLabelVerticalAlignment',
)
def __init__(self, **keywords) -> None:
extm = getExtendedModules()
self.plt = extm.plt # wrapper to matplotlib.pyplot
self.data: list | None = None
self.figure: matplotlib.figure.Figure | None = None
self.subplot: matplotlib.axes.Axes | None = None
# define a component dictionary for each axis
self.axis: dict[str, dict] = {}
for ax in self.axisKeys:
self.axis[ax] = {}
self.axis[ax]['range'] = None
self.grid: bool = True
self.axisRangeHasBeenSet: dict[str, bool] = {}
for axisKey in self.axisKeys:
self.axisRangeHasBeenSet[axisKey] = False
self.alpha: float = 0.2
self.dpi: int | None = None # determine on its own
self.colorBackgroundData: str = '#ffffff' # color of the data region
self.colorBackgroundFigure: str = '#ffffff' # looking good are #c7d2d4, #babecf
self.colorGrid: str = '#dddddd' # grid color
self.title: str = 'Music21 Graph'
self.figureSize: tuple[int | float, ...] = self.figureSizeDefault
self.marker: str = 'o'
self.markersize: int = 6 # lowercase as in matplotlib
# all default colors are on the slate-side of colors.
self.colors: list[str] = [
'#605c7f', # purple
'#5c7f60', # green
'#988969', # khaki
'#628297', # cyan
'#ad776d', # pink,
'#80a364', # lime,
]
self.tickFontSize: int = 7
self.tickColors: dict[str, str] = {'x': '#000000', 'y': '#000000'}
self.titleFontSize: int = 12
self.labelFontSize: int = 10
self.fontFamily: str = 'serif'
self.hideXGrid: bool = False
self.hideYGrid: bool = False
self.xTickLabelRotation: int = 0
self.xTickLabelHorizontalAlignment: str = 'center'
self.xTickLabelVerticalAlignment: str = 'center'
self.hideLeftBottomSpines: bool = False
self._doneAction: str | None = 'write'
self._dataColorIndex: int = 0
for kw in self.keywordConfigurables:
if kw in keywords:
setattr(self, kw, keywords[kw])
def __del__(self) -> None:
'''
Matplotlib Figure objects need to be explicitly closed when no longer used.
'''
if hasattr(self, 'figure') and self.figure is not None:
self.plt.close(self.figure)
def __getstate__(self) -> dict:
'''
The wrapper to matplotlib.pyplot stored as self.plt cannot be pickled/deepcopied.
'''
state = self.__dict__.copy()
del state['plt']
return state
def __setstate__(self, state: dict) -> None:
self.__dict__.update(state)
extm = getExtendedModules()
self.plt = extm.plt
@property
def doneAction(self) -> str | None:
'''
returns or sets what should happen when the graph is created (see docs above)
default is 'write'.
'''
return self._doneAction
@doneAction.setter
def doneAction(self, action: str | None) -> None:
if action in ('show', 'write', None):
self._doneAction = action
else: # pragma: no cover
raise GraphException(f'no such done action: {action}')
[docs]
def nextColor(self) -> str:
'''
Utility function that cycles through the colors of self.colors.
>>> g = graph.primitives.Graph()
>>> g.colors = ['#605c7f', '#5c7f60', '#715c7f']
>>> g.nextColor()
'#605c7f'
>>> g.nextColor()
'#5c7f60'
>>> g.nextColor()
'#715c7f'
>>> g.nextColor()
'#605c7f'
'''
c = getColor(self.colors[self._dataColorIndex % len(self.colors)])
self._dataColorIndex += 1
return c
[docs]
def setTicks(self,
axisKey: str,
pairs: t.Iterable[t.Sequence[object]] | None) -> None:
'''
Set the tick-labels for a given graph or plot's axisKey
(generally 'x', and 'y') with a set of pairs
Pairs are iterables of positions and labels.
N.B. -- both 'x' and 'y' ticks have to be set in
order to get matplotlib to display either (and presumably 'z' for 3D graphs).
>>> g = graph.primitives.GraphHorizontalBar()
>>> g.axis['x']['ticks']
Traceback (most recent call last):
KeyError: 'ticks'
>>> g.axis['x']
{'range': None}
>>> g.setTicks('x', [(0, 'a'), (1, 'b')])
>>> g.axis['x']['ticks']
([0, 1], ['a', 'b'])
>>> g.setTicks('m', [('a', 'b')])
Traceback (most recent call last):
music21.graph.utilities.GraphException: Cannot find key 'm' in self.axis
>>> g.setTicks('x', [])
>>> g.axis['x']['ticks']
([], [])
'''
if pairs is None: # is okay to send an empty list to clear everything
return
if axisKey not in self.axis:
raise GraphException(f"Cannot find key '{axisKey}' in self.axis")
positions = []
labels = []
# ticks are value, label pairs
for value, label in pairs:
positions.append(value)
labels.append(label)
# environLocal.printDebug(['got labels', labels])
self.axis[axisKey]['ticks'] = positions, labels
[docs]
def setIntegerTicksFromData(self,
unsortedData: t.Sequence[int | float],
axisKey: str = 'y',
dataSteps: int = 8) -> None:
'''
Set the ticks for an axis (usually 'y') given unsorted data.
Data steps shows how many ticks to make from the data.
>>> g = graph.primitives.GraphHorizontalBar()
>>> g.setIntegerTicksFromData([10, 5, 3, 8, 20, 11], dataSteps=4)
>>> g.axis['y']['ticks']
([0, 5, 10, 15, 20], ['0', '5', '10', '15', '20'])
TODO: should this not also use min? instead of always starting from zero?
'''
maxData = int(max(unsortedData))
tickStep = round(maxData / dataSteps)
tickList: list[tuple[int, str]] = []
if tickStep <= 1:
tickStep = 2
for y in range(0, maxData + 1, tickStep):
tickList.append((y, f'{y}'))
self.setTicks(axisKey, tickList)
[docs]
def setAxisRange(self,
axisKey: str,
valueRange: tuple[int | float, int | float],
paddingFraction: float = 0.1) -> None:
'''
Set the range for the axis for a given axis key
(generally, 'x', or 'y')
ValueRange is a two-element tuple of the lowest
number and the highest.
By default, there is a padding of 10% of the range
in either direction. Set paddingFraction = 0 to
eliminate this shift
'''
if axisKey not in self.axisKeys: # pragma: no cover
raise GraphException(f'No such axis exists: {axisKey}')
# find a shift
if paddingFraction != 0:
totalRange = valueRange[1] - valueRange[0]
shift = totalRange * paddingFraction # add 10 percent of range
else:
shift = 0
# set range with shift
self.axis[axisKey]['range'] = (valueRange[0] - shift,
valueRange[1] + shift)
self.axisRangeHasBeenSet[axisKey] = True
[docs]
def setAxisLabel(self, axisKey: str, label: str, conditional: bool = False) -> None:
if axisKey not in self.axisKeys: # pragma: no cover
raise GraphException(f'No such axis exists: {axisKey}')
if conditional and 'label' in self.axis[axisKey] and self.axis[axisKey]['label']:
return
self.axis[axisKey]['label'] = label
[docs]
@staticmethod
def hideAxisSpines(subplot: matplotlib.axes.Axes, leftBottom: bool = False) -> None:
'''
Remove the right and top spines from the diagram.
If leftBottom is True, remove the left and bottom spines as well.
Spines are removed by setting their colors to 'none' and every other
tick line set_visible to False.
'''
for loc, spine in subplot.spines.items():
if loc in ('left', 'bottom'):
if leftBottom:
spine.set_color('none') # don't draw spine
# # this pushes them outward in an interesting way
# spine.set_position(('outward', 10)) # outward by 10 points
elif loc in ('right', 'top'):
spine.set_color('none') # don't draw spine
else: # pragma: no cover
raise ValueError(f'unknown spine location: {loc}')
# remove top and right ticks
for i, line in enumerate(subplot.get_xticklines() + subplot.get_yticklines()):
if leftBottom:
line.set_visible(False)
elif i % 2 == 1: # top and right are the odd indices
line.set_visible(False)
# subplot.set_xscale('linear')
# subplot.set_yscale('linear')
# subplot.set_aspect('normal')
[docs]
def applyGrid(self, subplot: matplotlib.axes.Axes) -> None:
'''
Apply the Grid to the subplot such that it goes below the data.
'''
if self.grid and self.colorGrid is not None: # None is another way to hide grid
subplot.set_axisbelow(True)
subplot.grid(True, which='major', color=getColor(self.colorGrid))
# provide control for each grid line
if self.hideYGrid:
subplot.yaxis.grid(False)
if self.hideXGrid:
subplot.xaxis.grid(False)
# noinspection SpellCheckingInspection
[docs]
def process(self) -> None:
'''
Creates the figure and subplot, calls renderSubplot to get the
subclass specific information on the data, runs hideAxisSpines,
applyFormatting, and then calls the done action. Returns None,
but the subplot is available at self.subplot
'''
extm = getExtendedModules()
plt = extm.plt
# figure size can be set w/ figsize=(5, 10)
# if self.doneAction is None:
# extm.matplotlib.interactive(False)
self.figure = plt.figure(facecolor=self.colorBackgroundFigure)
self.subplot = self.figure.add_subplot(1, 1, 1)
self._dataColorIndex = 0 # just for consistent rendering if run twice
# call class specific info
self.renderSubplot(self.subplot)
# standard procedures
self.hideAxisSpines(self.subplot, leftBottom=self.hideLeftBottomSpines)
self.applyFormatting(self.subplot)
self.callDoneAction()
# if self.doneAction is None:
# extm.matplotlib.interactive(False)
[docs]
def renderSubplot(self, subplot: matplotlib.axes.Axes) -> None:
'''
Calls the subclass specific information to get the data
'''
pass
# --------------------------------------------------------------------------
[docs]
def callDoneAction(self, fp: str | None = None) -> None:
'''
Implement the desired doneAction, after data processing
'''
if self.doneAction == 'show': # pragma: no cover
self.show()
elif self.doneAction == 'write': # pragma: no cover
self.write(fp)
elif self.doneAction is None:
pass
[docs]
def show(self) -> None: # pragma: no cover
'''
Calls the show() method of the matplotlib plot.
For most matplotlib back ends, this will open
a GUI window with the desired graph.
'''
if self.figure is None:
raise ValueError('Cannot show graph without a figure; call process() first')
assert self.figure is not None
self.figure.show()
[docs]
def write(self,
fp: str | pathlib.Path | None = None
) -> matplotlib.figure.Figure | None: # pragma: no cover
'''
Writes the graph to a file. If no file path is given, a temporary file is used.
'''
if self.figure is None:
raise ValueError('Cannot write graph without a figure; call process() first')
assert self.figure is not None
if fp is None:
fp = environLocal.getTempFile('.png')
dpi = self.dpi
if dpi is None:
dpi = 300
self.figure.savefig(fp,
# facecolor=getColor(self.colorBackgroundData),
# edgecolor=getColor(self.colorBackgroundFigure),
dpi=dpi)
if not common.runningInNotebook():
SubConverter().launch(pathlib.Path(fp), fmt='png')
return None
else:
return self.figure
[docs]
class GraphColorGrid(Graph):
'''
Grid of discrete colored "blocks" to visualize results of a windowed analysis routine.
Data is provided as a list of lists of colors, where colors are specified as a hex triplet,
or the common HTML color codes, and based on analysis-specific mapping of colors to results.
>>> #_DOCS_SHOW g = graph.primitives.GraphColorGrid()
>>> g = graph.primitives.GraphColorGrid(doneAction=None) #_DOCS_HIDE
>>> data = [['#55FF00', '#9b0000', '#009b00'],
... ['#FFD600', '#FF5600'],
... ['#201a2b', '#8f73bf', '#a080d5', '#403355', '#999999']]
>>> g.data = data
>>> g.process()
.. image:: images/GraphColorGrid.*
:width: 600
'''
_DOC_ATTR: dict[str, str] = {
'hideLeftBottomSpines': 'bool to hide the left and bottom axis spines; default True',
}
graphType = 'colorGrid'
figureSizeDefault = (9, 6)
keywordConfigurables: tuple[str, ...] = Graph.keywordConfigurables + ('hideLeftBottomSpines',)
def __init__(self, **keywords) -> None:
self.hideLeftBottomSpines = True
super().__init__(**keywords)
[docs]
def renderSubplot(self, subplot: matplotlib.axes.Axes) -> None:
# do not need a grid for the outer container
if self.figure is None:
raise ValueError('Cannot render subplot without a figure; call process() first')
assert self.figure is not None
if self.data is None:
raise ValueError('Cannot render subplot without data; '
'set self.data before calling process()')
assert self.data is not None
# these approaches do not work:
# adjust face color of axTop independently
# this sets the color of the main data presentation window
# axTop.patch.set_facecolor('#000000')
# axTop.bar([0.5], [1], 1, color=['#000000'], linewidth=0.5, edgecolor='#111111')
self.figure.subplots_adjust(left=0.15)
rowCount = len(self.data)
for i in range(rowCount):
thisRowData = self.data[i]
positions = []
heights = []
subColors = []
for j, thisColor in enumerate(thisRowData):
positions.append(j + 1 / 2)
# collect colors in a list to set all at once
subColors.append(thisColor)
# correlations.append(float(self.data[i][j][2]))
heights.append(1)
# add a new subplot for each row
ax = self.figure.add_subplot(rowCount, 1, rowCount - i)
# linewidth: 0.1 is the thinnest possible
# antialiased = false, for small diagrams, provides tighter images
ax.bar(positions,
heights,
1,
color=subColors,
linewidth=0.3,
edgecolor='#000000',
antialiased=False)
# remove spines from each bar plot; cause excessive thickness
for unused_loc, spine in ax.spines.items():
# spine.set_color('none') # don't draw spine
spine.set_linewidth(0.3)
spine.set_color('#000000')
spine.set_alpha(1)
# remove all ticks for subplots
for j, line in enumerate(ax.get_xticklines() + ax.get_yticklines()):
line.set_visible(False)
ax.set_xticks([])
ax.set_yticks([])
ax.set_yticklabels([''] * len(ax.get_yticklabels()))
ax.set_xticklabels([''] * len(ax.get_xticklabels()))
# this is the shifting the visible bars; may not be necessary
ax.set_xlim((0, len(self.data[i])))
# these do not seem to do anything
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
# adjust space between the bars
# 0.1 is about the smallest that gives some space
if rowCount > 12:
self.figure.subplots_adjust(hspace=0)
else:
self.figure.subplots_adjust(hspace=0.1)
axisRangeNumbers = (0, 1)
self.setAxisRange('x', axisRangeNumbers, 0)
# turn off grid
self.grid = False
[docs]
class GraphColorGridLegend(Graph):
'''
Grid of discrete colored "blocks" where each block can be labeled
Data is provided as a list of lists of colors, where colors are specified as a hex triplet,
or the common HTML color codes, and based on analysis-specific mapping of colors to results.
>>> #_DOCS_SHOW g = graph.primitives.GraphColorGridLegend()
>>> g = graph.primitives.GraphColorGridLegend(doneAction=None) #_DOCS_HIDE
>>> data = [
... ('Major', [('C#', '#00aa55'), ('D-', '#5600ff'), ('G#', '#2b00ff')]),
... ('Minor', [('C#', '#004600'), ('D-', '#00009b'), ('G#', '#00009b')])
... ]
>>> g.data = data
>>> g.process()
.. image:: images/GraphColorGridLegend.*
:width: 600
'''
_DOC_ATTR: dict[str, str] = {
'hideLeftBottomSpines': 'bool to hide the left and bottom axis spines; default True',
}
graphType = 'colorGridLegend'
figureSizeDefault = (5, 1.5)
keywordConfigurables: tuple[str, ...] = Graph.keywordConfigurables + ('hideLeftBottomSpines',)
def __init__(self, **keywords) -> None:
self.hideLeftBottomSpines = True
super().__init__(**keywords)
if 'title' not in keywords:
self.title = 'Legend'
[docs]
def renderSubplot(self, subplot: matplotlib.axes.Axes) -> None:
if self.figure is None:
raise ValueError('Cannot render subplot without a figure; call process() first')
assert self.figure is not None
if self.data is None:
raise ValueError('Cannot render subplot without data; '
'set self.data before calling process()')
assert self.data is not None
for i, rowLabelAndData in enumerate(self.data):
rowLabel = rowLabelAndData[0]
rowData = rowLabelAndData[1]
self.makeOneRowOfGraph(self.figure, i, rowLabel, rowData)
self.setAxisRange('x', (0, 1), 0)
allTickLines = subplot.get_xticklines() + subplot.get_yticklines()
for j, line in enumerate(allTickLines):
line.set_visible(False)
# sets the space between subplots
# top and bottom here push diagram more toward center of frame
# may be useful in other graphs
# ,
self.figure.subplots_adjust(hspace=1.5, top=0.75, bottom=0.2)
self.setAxisLabel('y', '')
self.setAxisLabel('x', '')
self.setTicks('y', [])
self.setTicks('x', [])
[docs]
def makeOneRowOfGraph(self,
figure: matplotlib.figure.Figure,
rowIndex: int,
rowLabel: str,
rowData: list[tuple[str, str]]) -> matplotlib.axes.Axes:
# noinspection PyShadowingNames
'''
Makes a subplot for one row of data (such as for the Major label)
and returns a matplotlib.axes.AxesSubplot instance representing the subplot.
Here we create an axis with a part of Scriabin's mapping of colors
to keys in Prometheus: The Poem of Fire.
>>> import matplotlib.pyplot
>>> colorLegend = graph.primitives.GraphColorGridLegend()
>>> rowData = [('C', '#ff0000'), ('G', '#ff8800'), ('D', '#ffff00'),
... ('A', '#00ff00'), ('E', '#4444ff')]
>>> colorLegend.data = [['Scriabin Mapping', rowData]]
>>> fig = matplotlib.pyplot.figure()
>>> subplot = colorLegend.makeOneRowOfGraph(fig, 0, 'Scriabin Mapping', rowData)
>>> subplot
<Axes: >
'''
if self.data is None:
raise ValueError('Cannot create row of graph without data; '
'set self.data before calling process()')
assert self.data is not None
# environLocal.printDebug(['rowLabel', rowLabel, i])
positions = []
heights = []
subColors = []
for j, oneColorMapping in enumerate(rowData):
positions.append(1.0 + j)
subColors.append(oneColorMapping[1]) # second value is colors
heights.append(1)
# add a new subplot for each row
posTriple = (len(self.data), 1, rowIndex + 1)
# environLocal.printDebug(['posTriple', posTriple])
ax = figure.add_subplot(*posTriple)
# ax is an Axes object
# 1 here is width
width = 1
ax.bar(positions, heights, width, color=subColors, linewidth=0.3, edgecolor='#000000')
# lower thickness of spines
for spineArtist in ax.spines.values():
# spineArtist.set_color('none') # don't draw spine
spineArtist.set_linewidth(0.3)
spineArtist.set_color('#000000')
# remove all ticks for subplots
allTickLines = ax.get_xticklines() + ax.get_yticklines()
for j, line in enumerate(allTickLines):
line.set_visible(False)
# need one label for each left side; 0.5 is in the middle
ax.set_yticks([0.5])
ax.set_yticklabels([rowLabel],
fontsize=self.tickFontSize,
family=self.fontFamily,
horizontalalignment='right',
verticalalignment='center') # one label for one tick
# need a label for each bars
ax.set_xticks([x + 1 for x in range(len(rowData))])
# get labels from row data; first of pair
# need to push y down as need bottom alignment for lower case
substitutedAccidentalLabels = [accidentalLabelToUnicode(x)
for x, unused_y in rowData]
ax.set_xticklabels(
substitutedAccidentalLabels,
fontsize=self.tickFontSize,
family=self.fontFamily,
horizontalalignment='center',
verticalalignment='center',
y=-0.4)
# this is the scaling to see all bars; not necessary
ax.set_xlim((0.5, len(rowData) + 0.5))
return ax
[docs]
class GraphHorizontalBar(Graph):
'''
Numerous horizontal bars in discrete channels, where bars
can be incomplete and/or overlap.
Data provided is a list of pairs, where the first value becomes the key,
the second value is a list of x-start, x-length values.
Note how the second element in each data point is the length, so
subtracting death year from birth year gives the appropriate length.
Example: Plot the life-span of four composers whose lives were intertwined:
Chopin, Robert and Clara Schumann, and Brahms.
>>> a = graph.primitives.GraphHorizontalBar()
>>> a.doneAction = None #_DOCS_HIDE
>>> data = [('Chopin', [(1810, 1849-1810)]),
... ('Schumanns', [(1810, 1856-1810), (1819, 1896-1819)]),
... ('Brahms', [(1833, 1897-1833)])]
>>> a.data = data
>>> a.process()
.. image:: images/GraphHorizontalBar.*
:width: 600
Data is a list of tuples in the form, where each entry represents a space on the
Y axis:
* Label
* List of tuples of numeric data where each tuple has two or three elements:
* Start x-position
* Length of bar
* Optional: dictionary of format information about this point.
* Optional: dictionary of format information for all points at this level.
(this will be overridden by any information for the particular point)
To make an equally spaced plot, like in a Pitch Space plot, leave empty data in the form:
`('', [], {})`
'''
_DOC_ATTR: dict[str, str] = {
'barSpace': 'Amount of vertical space each bar takes; default 8',
'margin': '''
Vertical space above and below the bars, default 2 (= total4 space between bars)
''',
}
graphType = 'horizontalBar'
figureSizeDefault = (10, 4)
keywordConfigurables: tuple[str, ...] = Graph.keywordConfigurables + (
'barSpace',
'margin',
)
def __init__(self, **keywords) -> None:
self.barSpace: int = 8
self.margin: int = 2
super().__init__(**keywords)
if 'alpha' not in keywords:
self.alpha = 0.6
@property
def barHeight(self):
return self.barSpace - (self.margin * 2)
[docs]
def renderSubplot(self, subplot: matplotlib.axes.Axes) -> None:
if self.figure is None:
raise ValueError('Cannot render subplot without a figure; call process() first')
assert self.figure is not None
if self.data is None:
raise ValueError('Cannot render subplot without data; '
'set self.data before calling process()')
assert self.data is not None
self.figure.subplots_adjust(left=0.15)
yPos = 0
xPoints: set[int|float] = set() # store all to find min/max
yTicks = [] # a list of label, value pairs
xTicks = []
keys = []
i = 0
# TODO: check data orientation; flips in some cases
for info in self.data:
if len(info) == 2:
key, points = info
fullRowFormatDict = {}
else:
key, points, fullRowFormatDict = info
keys.append(key)
# provide a list of start, end points;
# then start y position, bar height
faceColor = fullRowFormatDict.get('color', self.nextColor())
if points:
uniformFormatPerRow = (len(points[0]) == 2)
rowFaceColors: str|list[str]
if uniformFormatPerRow:
rowFaceColors = faceColor
positionPoints = points
else:
rowFaceColors = [p[2].get('color', faceColor) for p in points]
positionPoints = [p[:2] for p in points]
yRange = (yPos + self.margin,
self.barHeight)
subplot.broken_barh(positionPoints,
yRange,
facecolors=rowFaceColors,
alpha=self.alpha)
for p in points:
if len(p) >= 2:
xStart, xLen = p[:2]
else:
raise ValueError(f'Points must be length 2 or 3, not {len(p)}: {p}')
xEnd = xStart + xLen
for x in [xStart, xEnd]:
if x not in xPoints:
xPoints.add(x)
# ticks are value, label
yTicks.append([yPos + self.barSpace * 0.5, key])
# yTicks.append([key, yPos + self.barSpace * 0.5])
yPos += self.barSpace
i += 1
xMin: int|float = 0.0
xMax: int | float = 0.0
if xPoints: # do not crash on empty streams
xMin = min(xPoints)
xMax = max(xPoints)
xRange = xMax - xMin
# environLocal.printDebug(['got xMin, xMax for points', xMin, xMax, ])
self.setAxisRange('y', (0, len(keys) * self.barSpace))
self.setAxisRange('x', (xMin, xMax))
self.setTicks('y', yTicks)
# first, see if ticks have been set externally
if 'ticks' in self.axis['x'] and not self.axis['x']['ticks']:
rangeStep = int(xMin + round(xRange / 10))
if rangeStep == 0:
rangeStep = 1
for x in range(int(math.floor(xMin)),
round(math.ceil(xMax)),
rangeStep):
xTicks.append([x, f'{x}'])
self.setTicks('x', xTicks)
[docs]
class GraphHorizontalBarWeighted(Graph):
'''
Numerous horizontal bars in discrete channels,
where bars can be incomplete and/or overlap, and
can have different heights and colors within their
respective channel.
'''
_DOC_ATTR: dict[str, str] = {
'barSpace': 'Amount of vertical space each bar takes; default 8',
'margin': 'Space around the bars, default 2',
}
graphType = 'horizontalBarWeighted'
figureSizeDefault = (10, 4)
keywordConfigurables: tuple[str, ...] = Graph.keywordConfigurables + (
'barSpace',
'margin',
)
def __init__(self, **keywords) -> None:
self.barSpace: int = 8
self.margin: float = 0.25 # was 8; determines space between channels
super().__init__(**keywords)
# this default alpha is used if not specified per bar
if 'alpha' not in keywords:
self.alpha = 1
# example data
# data = [
# ('Violins', [(3, 5, 1, '#fff000'), (1, 12, 0.2, '#3ff203')] ),
# ('Celli', [(2, 7, 0.2, '#0ff302'), (10, 3, 0.6, '#ff0000', 1)] ),
# ('Clarinet', [(5, 1, 0.5, '#3ff203')] ),
# ('Flute', [(5, 1, 0.1, '#00ff00'), (7, 20, 0.3, '#00ff88')] ),
# ]
@property
def barHeight(self):
return self.barSpace - (self.margin * 2)
[docs]
def renderSubplot(self, subplot: matplotlib.axes.Axes) -> None:
if self.figure is None:
raise ValueError('Cannot render subplot without a figure; call process() first')
assert self.figure is not None
if self.data is None:
raise ValueError('Cannot render subplot without data; '
'set self.data before calling process()')
assert self.data is not None
# might need more space here for larger y-axis labels
self.figure.subplots_adjust(left=0.15)
yPos = 0
xPoints = [] # store all to find min/max
yTicks = [] # a list of label, value pairs
# xTicks = []
keys = []
i = 0
# reversing data to present in order
self.data = list(self.data)
self.data.reverse()
for key, points in self.data:
keys.append(key)
xRanges = []
yRanges = []
alphas = []
colors = []
for i, data in enumerate(points):
x = 0
span = None
heightScalar = 1
color = self.nextColor()
alpha = self.alpha
yShift = 0 # between -1 and 1
if len(data) == 3:
x, span, heightScalar = data
elif len(data) == 4:
x, span, heightScalar, color = data
elif len(data) == 5:
x, span, heightScalar, color, alpha = data
elif len(data) == 6:
x, span, heightScalar, color, alpha, yShift = data
# filter color value
color = getColor(color)
if span is None:
raise ValueError(f'Data point must have at least 3 elements '
f'(x, span, heightScalar, ...); got: {data}')
assert span is not None
# add to x ranges
xRanges.append((x, span))
colors.append(color)
alphas.append(alpha)
# x points used to get x ticks
if x not in xPoints:
xPoints.append(x)
if (x + span) not in xPoints:
xPoints.append(x + span)
# TODO: add high/low shift to position w/n range
# provide a list of start, end points;
# then start y position, bar height
h = self.barHeight * heightScalar
yAdjust = (self.barHeight - h) * 0.5
yShiftUnit = self.barHeight * (1 - heightScalar) * 0.5
adjustedY = yPos + self.margin + yAdjust + (yShiftUnit * yShift)
yRanges.append((adjustedY, h))
for i, xRange in enumerate(xRanges):
# note: can get rid of bounding lines by providing
# linewidth=0, however, this may leave gaps in adjacent regions
subplot.broken_barh([xRange],
yRanges[i],
facecolors=colors[i],
alpha=alphas[i],
edgecolor=colors[i])
# ticks are value, label
yTicks.append([yPos + self.barSpace * 0.5, key])
# yTicks.append([key, yPos + self.barSpace * 0.5])
yPos += self.barSpace
i += 1
xMin = min(xPoints)
xMax = max(xPoints)
# environLocal.printDebug(['got xMin, xMax for points', xMin, xMax, ])
# NOTE: these pad values determine extra space inside the graph that
# is not filled with data, a sort of inner margin
self.setAxisRange('y', (0, len(keys) * self.barSpace), paddingFraction=0.05)
self.setAxisRange('x', (xMin, xMax), paddingFraction=0.01)
self.setTicks('y', yTicks)
# # first, see if ticks have been set externally
# if 'ticks' in self.axis['x'] and len(self.axis['x']['ticks']) == 0:
# rangeStep = int(xMin round(xRange/10))
# if rangeStep == 0:
# rangeStep = 1
# for x in range(int(math.floor(xMin)),
# round(math.ceil(xMax)),
# rangeStep):
# xTicks.append([x, f'{x}'])
# self.setTicks('x', xTicks)
# environLocal.printDebug(['xTicks', xTicks])
[docs]
class GraphScatterWeighted(Graph):
'''
A scatter plot where points are scaled in size to
represent the number of values stored within.
>>> g = graph.primitives.GraphScatterWeighted()
>>> g.doneAction = None #_DOCS_HIDE
>>> data = [(23, 15, 234), (10, 23, 12), (4, 23, 5), (15, 18, 120)]
>>> g.data = data
>>> g.process()
.. image:: images/GraphScatterWeighted.*
:width: 600
'''
_DOC_ATTR: dict[str, str] = {
'maxDiameter': 'the maximum diameter of any ellipse, default 1.25',
'minDiameter': 'the minimum diameter of any ellipse, default 0.25',
}
graphType = 'scatterWeighted'
figureSizeDefault = (5, 5)
keywordConfigurables: tuple[str, ...] = Graph.keywordConfigurables + (
'maxDiameter', 'minDiameter')
def __init__(self, **keywords) -> None:
self.maxDiameter: float = 1.25
self.minDiameter: float = 0.25
super().__init__(**keywords)
if 'alpha' not in keywords:
self.alpha = 0.6
@property
def rangeDiameter(self):
return self.maxDiameter - self.minDiameter
[docs]
def renderSubplot(self, subplot: matplotlib.axes.Axes) -> None:
if self.figure is None:
raise ValueError('Cannot render subplot without a figure; call process() first')
assert self.figure is not None
if self.data is None:
raise ValueError('Cannot render subplot without data; '
'set self.data before calling process()')
assert self.data is not None
extm = getExtendedModules()
patches = extm.patches
# these need to be equal to maintain circle scatter points
self.figure.subplots_adjust(left=0.15, bottom=0.15)
# need to filter data to weight z values
xList = [d[0] for d in self.data]
yList = [d[1] for d in self.data]
zList = [d[2] for d in self.data]
formatDictList = []
for d in self.data:
if len(d) > 3:
formatDict = d[3]
else:
formatDict = {}
formatDictList.append(formatDict)
xMax = max(xList)
xMin = min(xList)
xRange = float(xMax - xMin)
yMax = max(yList)
yMin = min(yList)
yRange = float(yMax - yMin)
zMax = max(zList)
zMin = min(zList)
zRange = float(zMax - zMin)
# if xRange and yRange are not the same, the resulting circle,
# when drawn, will be distorted into an ellipse. to counter this
# we need to get a ratio to scale the width of the ellipse
xDistort: float = 1.0
yDistort: float = 1.0
if xRange > yRange:
yDistort = yRange / xRange
elif yRange > xRange:
xDistort = xRange / yRange
# environLocal.printDebug(['xDistort, yDistort', xDistort, yDistort])
zNorm = []
for z in zList:
if z == 0:
zNorm.append([0, 0])
else:
# this will make the minimum scalar 0 when z is zero
if zRange != 0:
scalar = (z - zMin) / zRange # shifted part / range
else:
scalar = 0.5 # if all the same size, use 0.5
scaled = self.minDiameter + (self.rangeDiameter * scalar)
zNorm.append([scaled, scalar])
# draw ellipses
for i in range(len(self.data)):
x = xList[i]
y = yList[i]
z, unused_zScalar = zNorm[i] # normalized values
formatDict = formatDictList[i]
width = z * xDistort
height = z * yDistort
e = patches.Ellipse(xy=(x, y), width=width, height=height, **formatDict)
# e = patches.Circle(xy=(x, y), radius=z)
subplot.add_artist(e)
e.set_clip_box(subplot.bbox)
# e.set_alpha(self.alpha * zScalar)
e.set_alpha(self.alpha)
e.set_facecolor(self.nextColor())
# # can do this here
# environLocal.printDebug([e])
# only show label if min is greater than zNorm min
if zList[i] > 1:
# xdistort does not seem to
# width shift can be between 0.1 and 0.25
# width is already shifted by distort
# use half of width == radius
adjustedX = x + ((width * 0.5) + (0.05 * xDistort))
adjustedY = y + 0.10 # why?
subplot.text(adjustedX,
adjustedY,
str(zList[i]),
size=6,
va='baseline',
ha='left',
multialignment='left')
self.setAxisRange('y', (yMin, yMax))
self.setAxisRange('x', (xMin, xMax))
[docs]
class GraphScatter(Graph):
'''
Graph two parameters in a scatter plot.
Data representation is a list of points of values.
>>> g = graph.primitives.GraphScatter()
>>> g.doneAction = None #_DOCS_HIDE
>>> data = [(x, x * x) for x in range(50)]
>>> g.data = data
>>> g.process()
.. image:: images/GraphScatter.*
:width: 600
'''
graphType = 'scatter'
[docs]
def renderSubplot(self, subplot: matplotlib.axes.Axes) -> None:
if self.figure is None:
raise ValueError('Cannot render subplot without a figure; call process() first')
assert self.figure is not None
if self.data is None:
raise ValueError('Cannot render subplot without data; '
'set self.data before calling process()')
assert self.data is not None
self.figure.subplots_adjust(left=0.15)
xValues = []
yValues = []
i = 0
for row in self.data:
if len(row) < 2: # pragma: no cover
raise GraphException('Need at least two points for a graph data object!')
x = row[0]
y = row[1]
xValues.append(x)
yValues.append(y)
marker = self.marker
color = self.nextColor()
alpha = self.alpha
markersize = self.markersize
if len(row) >= 3:
displayData = row[2]
if 'color' in displayData:
color = displayData['color']
if 'marker' in displayData:
marker = displayData['marker']
if 'alpha' in displayData:
alpha = displayData['alpha']
if 'markersize' in displayData:
markersize = displayData['markersize']
subplot.plot(x, y, marker=marker, color=color, alpha=alpha, markersize=markersize)
i += 1
if not self.axisRangeHasBeenSet['y']:
self.setAxisRange('y', (min(yValues), max(yValues)))
if not self.axisRangeHasBeenSet['x']:
self.setAxisRange('x', (min(xValues), max(xValues)))
[docs]
class GraphHistogram(Graph):
'''
Graph the count of a single element.
Data set is simply a list of x and y pairs, where there
is only one of each x value, and y value is the count or magnitude
of that value
>>> import random
>>> g = graph.primitives.GraphHistogram()
>>> g.doneAction = None #_DOCS_HIDE
>>> g.graphType
'histogram'
>>> data = [(x, random.choice(range(30))) for x in range(50)]
>>> g.data = data
>>> g.process()
.. image:: images/GraphHistogram.*
:width: 600
'''
_DOC_ATTR: dict[str, str] = {
'binWidth': '''
Size of each bin; if the bins are equally spaced at intervals of 1,
then 0.8 is a good default to allow a little space. 1.0 will give no
space.
''',
}
graphType = 'histogram'
keywordConfigurables: tuple[str, ...] = Graph.keywordConfigurables + ('binWidth',)
def __init__(self, *, binWidth: float = 0.8, alpha: float = 0.8, **keywords):
self.binWidth = binWidth
super().__init__(alpha=alpha, **keywords)
[docs]
def renderSubplot(self, subplot: matplotlib.axes.Axes) -> None:
if self.figure is None:
raise ValueError('Cannot render subplot without a figure; call process() first')
assert self.figure is not None
if self.data is None:
raise ValueError('Cannot render subplot without data; '
'set self.data before calling process()')
assert self.data is not None
self.figure.subplots_adjust(left=0.15)
x = []
y = []
binWidth = self.binWidth
color = getColor(self.colors[0])
alpha = self.alpha
# TODO: use the formatDict!
for point in self.data:
if len(point) > 2:
a, b, unused_formatDict = point
else:
a, b = point
x.append(a)
y.append(b)
subplot.bar(x, y, width=binWidth, alpha=alpha, color=color)
[docs]
class GraphGroupedVerticalBar(Graph):
'''
Graph the count of one or more elements in vertical bars.
The `.data` is simply a list of x and y pairs, where there
is only one of each x value, and y value is a list of values.
>>> from collections import OrderedDict
>>> g = graph.primitives.GraphGroupedVerticalBar()
>>> g.doneAction = None #_DOCS_HIDE
>>> lengths = OrderedDict( [('a', 3), ('b', 2), ('c', 1)] )
>>> data = [('bar' + str(x), lengths) for x in range(3)]
>>> data
[('bar0', OrderedDict({'a': 3, 'b': 2, 'c': 1})),
('bar1', OrderedDict({'a': 3, 'b': 2, 'c': 1})),
('bar2', OrderedDict({'a': 3, 'b': 2, 'c': 1}))]
>>> g.data = data
>>> g.process()
'''
graphType = 'groupedVerticalBar'
keywordConfigurables: tuple[str, ...] = Graph.keywordConfigurables + (
'binWidth', 'roundDigits', 'groupLabelHeight',)
def __init__(self, **keywords) -> None:
self.binWidth: int | float = 1
self.roundDigits: int = 1
self.groupLabelHeight: float = 0.0
super().__init__(**keywords)
[docs]
def labelBars(self, subplot: matplotlib.axes.Axes, rects) -> None:
# attach some text labels
for rect in rects:
adjustedX = rect.get_x() + (rect.get_width() / 2)
height = rect.get_height()
subplot.text(adjustedX,
height,
str(round(height, self.roundDigits)),
ha='center',
va='bottom',
fontsize=self.tickFontSize,
family=self.fontFamily)
[docs]
def renderSubplot(self, subplot: matplotlib.axes.Axes) -> None:
if self.data is None:
raise ValueError('Cannot render subplot without data; '
'set self.data before calling process()')
assert self.data is not None
extm = getExtendedModules()
matplotlib = extm.matplotlib
barsPerGroup = 1
subLabels = []
# b value is a list of values for each bar
for unused_a, b in self.data:
barsPerGroup = len(b)
# get for legend
subLabels = sorted(b.keys())
break
binWidth = self.binWidth
widthShift = binWidth / barsPerGroup
xVals = []
yBundles = []
for i, (unused_a, b) in enumerate(self.data):
# create x vals from index values
xVals.append(i)
yBundles.append([b[key] for key in sorted(b.keys())])
rects = []
for i in range(barsPerGroup):
yVals = []
for j, _x in enumerate(xVals):
# get position, then get bar group
yVals.append(yBundles[j][i])
xValsShifted = []
# xLabels = []
for x in xVals:
xValsShifted.append(x + (widthShift * i))
rect = subplot.bar(xValsShifted,
yVals,
width=widthShift,
alpha=0.8,
color=self.nextColor())
rects.append(rect)
colors = []
for rect in rects:
self.labelBars(subplot, rect)
colors.append(rect[0])
fontProps = matplotlib.font_manager.FontProperties(size=self.tickFontSize,
family=self.fontFamily)
subplot.legend(colors, subLabels, prop=fontProps)
[docs]
class Graph3DBars(Graph):
'''
Graph multiple parallel bar graphs in 3D.
Data definition:
A list of lists where the inner list of
(x, y, z) coordinates.
For instance, a graph where the x values increase
(left to right), the y values increase in a step
pattern (front to back), and the z values decrease
(top to bottom):
>>> g = graph.primitives.Graph3DBars()
>>> g.doneAction = None #_DOCS_HIDE
>>> data = []
>>> for i in range(1, 10 + 1):
... q = [i, i//2, 10 - i]
... data.append(q)
>>> g.data = data
>>> g.process()
When creating the graph these keyword arguments can be used (in addition to any
such as `colors` that all Graph classes can use).
* ``alpha`` -- opacity of each bar, from 0.0 (transparent) to 1.0 (opaque).
Defaults to 0.8.
* ``barWidth`` -- width of each bar along the x-axis. When ``None``
(default) the width is computed automatically.
Individual data points may also override this
via a ``'barWidth'`` key in their per-point format dict.
* ``barDepth`` -- depth of each bar along the y-axis. When ``None``
(default) the depth is computed automatically.
Individual data points may also override this
via a ``'barDepth'`` key in their per-point format dict.
'''
graphType = '3DBars'
axisKeys = ('x', 'y', 'z')
def __init__(self,
*,
alpha: float = 0.8,
barWidth: float|int|None = None,
barDepth: float|int|None = None,
**keywords):
super().__init__(alpha=alpha, **keywords)
self.barWidth: float|int|None = barWidth
self.barDepth: float|int|None = barDepth
if 'colors' not in keywords:
self.colors = ['#ff0000', '#00ff00', '#6666ff']
[docs]
def process(self) -> None:
extm = getExtendedModules()
plt = extm.plt
self.figure = plt.figure()
self.subplot = self.figure.add_subplot(1, 1, 1, projection='3d')
self.renderSubplot(self.subplot)
self.applyFormatting(self.subplot)
self.callDoneAction()
[docs]
def renderSubplot(self, subplot: matplotlib.axes.Axes) -> None:
if self.data is None:
raise ValueError('Cannot render subplot without data; '
'set self.data before calling process()')
assert self.data is not None
xVals = []
yVals = []
zVals = []
for point in self.data:
x, y, z = point[0], point[1], point[2]
xVals.append(x)
yVals.append(y)
zVals.append(z)
xMin, xMax = min(xVals), max(xVals)
yMin, yMax = min(yVals), max(yVals)
zMin, zMax = min(zVals), max(zVals)
if self.axis['x']['range'] is None:
self.axis['x']['range'] = xMin, xMax
# swap y for z
if self.axis['z']['range'] is None:
self.axis['z']['range'] = zMin, zMax
if self.axis['y']['range'] is None:
self.axis['y']['range'] = yMin, yMax
barWidth = self.barWidth if self.barWidth is not None else (xMax - xMin) / 20
barDepth = self.barDepth if self.barDepth is not None else (yMax - yMin) / 20
for dataPoint in self.data:
if len(dataPoint) == 3:
x, y, z = dataPoint
formatDict = {}
elif len(dataPoint) == 4:
x, y, z, formatDict = dataPoint
else:
raise GraphException('Cannot plot a point unless it has 3 or 4 values')
if 'color' in formatDict:
color = formatDict['color']
else:
color = self.nextColor()
if 'barWidth' in formatDict:
barWidth = formatDict['barWidth']
if 'barDepth' in formatDict:
barDepth = formatDict['barDepth']
subplot.bar3d(x - (barWidth / 2), y - (barDepth / 2), 0, # type: ignore[attr-defined]
barWidth, barDepth, z,
color=color,
alpha=self.alpha)
self.setAxisLabel('x', 'x', conditional=True)
self.setAxisLabel('y', 'y', conditional=True)
self.setAxisLabel('z', 'z', conditional=True)
class Test(unittest.TestCase):
def testCopyAndDeepcopy(self):
from music21.test.commonTest import testCopyAll
testCopyAll(self, globals())
# ------------------------------------------------------------------------------
class TestExternal(unittest.TestCase):
show = True
def testBasic(self):
a = GraphScatter(doneAction=None, title='x to x*x', alpha=1)
data = [(x, x * x) for x in range(50)]
a.data = data
a.process()
a = GraphHistogram(doneAction=None, title='50 x with random(30) y counts')
data = [(x, random.choice(range(30))) for x in range(50)]
a.data = data
a.process()
a = Graph3DBars(doneAction=None,
title='50 x with random values increase by 10 per x',
alpha=0.8,
colors=['b', 'g'])
data = []
for i in range(1, 4):
q = [(x, random.choice(range(10 * i, 10 * (i + 1))), i) for x in range(50)]
data.extend(q)
a.data = data
a.process()
del a
def testBrokenHorizontal(self):
data = []
for label in ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']:
points = []
for i in range(10):
start = random.choice(range(150))
end = start + random.choice(range(50))
points.append((start, end))
data.append([label, points])
a = GraphHorizontalBar(doneAction=None)
a.data = data
a.process()
def testScatterWeighted(self):
data = []
for i in range(50):
x = random.choice(range(20))
y = random.choice(range(20))
z = random.choice(range(1, 20))
data.append([x, y, z])
if self.show:
doneAction = 'write'
else:
doneAction = None
a = GraphScatterWeighted(doneAction=doneAction)
a.data = data
a.process()
def x_test_writeAllGraphs(self):
'''
Write a graphic file for all graphs,
naming them after the appropriate class.
This is used to generate documentation samples.
'''
# get some data
data3DPolygonBars = []
for i in range(1, 4):
q = [(x, random.choice(range(10 * (i + 1))), i) for x in range(20)]
data3DPolygonBars.extend(q)
# pair data with class name
# noinspection SpellCheckingInspection
graphClasses = [
(GraphHorizontalBar,
[('Chopin', [(1810, 1849 - 1810)]),
('Schumanns', [(1810, 1856 - 1810), (1819, 1896 - 1819)]),
('Brahms', [(1833, 1897 - 1833)])]
),
(GraphScatterWeighted,
[(23, 15, 234), (10, 23, 12), (4, 23, 5), (15, 18, 120)]),
(GraphScatter,
[(x, x * x) for x in range(50)]),
(GraphHistogram,
[(x, random.choice(range(30))) for x in range(50)]),
(Graph3DBars, data3DPolygonBars),
(GraphColorGridLegend,
[('Major', [('C', '#00AA55'), ('D', '#5600FF'), ('G', '#2B00FF')]),
('Minor', [('C', '#004600'), ('D', '#00009b'), ('G', '#00009B')]), ]
),
(GraphColorGrid, [['#8968CD', '#96CDCD', '#CD4F39'],
['#FFD600', '#FF5600'],
['#201a2b', '#8f73bf', '#a080d5', '#6495ED', '#FF83FA'],
]
),
]
for graphClassName, data in graphClasses:
obj = graphClassName(doneAction=None)
obj.data = data # add data here
obj.process()
fn = obj.__class__.__name__ + '.png'
fp = str(environLocal.getRootTempDir() / fn)
environLocal.printDebug(['writing fp:', fp])
obj.write(fp)
def x_test_writeGraphColorGrid(self):
# this is temporary
a = GraphColorGrid(doneAction=None)
data = [['#525252', '#5f5f5f', '#797979', '#858585', '#727272', '#6c6c6c',
'#8c8c8c', '#8c8c8c', '#6c6c6c', '#999999', '#999999', '#797979',
'#6c6c6c', '#5f5f5f', '#525252', '#464646', '#3f3f3f', '#3f3f3f',
'#4c4c4c', '#4c4c4c', '#797979', '#797979', '#4c4c4c', '#4c4c4c',
'#525252', '#5f5f5f', '#797979', '#858585', '#727272', '#6c6c6c'],
['#999999', '#999999', '#999999', '#999999', '#999999', '#999999',
'#999999', '#999999', '#999999', '#999999', '#999999', '#797979',
'#6c6c6c', '#5f5f5f', '#5f5f5f', '#858585', '#797979', '#797979',
'#797979', '#797979', '#797979', '#797979', '#858585', '#929292', '#999999'],
['#999999', '#999999', '#999999', '#999999', '#999999', '#999999',
'#999999', '#999999', '#999999', '#999999', '#999999', '#999999',
'#8c8c8c', '#8c8c8c', '#8c8c8c', '#858585', '#797979', '#858585',
'#929292', '#999999'],
['#999999', '#999999', '#999999', '#999999', '#999999', '#999999',
'#999999', '#999999', '#999999', '#999999', '#999999', '#999999',
'#8c8c8c', '#929292', '#999999'],
['#999999', '#999999', '#999999', '#999999', '#999999', '#999999',
'#999999', '#999999', '#999999', '#999999'],
['#999999', '#999999', '#999999', '#999999', '#999999']]
a.data = data
a.process()
fn = a.__class__.__name__ + '.png'
fp = str(environLocal.getRootTempDir() / fn)
a.write(fp)
def x_test_writeGraphingDocs(self):
'''
Write graphing examples for the docs
'''
post = []
a = GraphScatter(doneAction=None)
data = [(x, x * x) for x in range(50)]
a.data = data
post.append([a, 'graphing-01'])
a = GraphScatter(title='Exponential Graph', alpha=1, doneAction=None)
data = [(x, x * x) for x in range(50)]
a.data = data
post.append([a, 'graphing-02'])
a = GraphHistogram(doneAction=None)
data = [(x, random.choice(range(30))) for x in range(50)]
a.data = data
post.append([a, 'graphing-03'])
a = Graph3DBars(doneAction=None)
data = []
for i in range(1, 4):
q = [(x, random.choice(range(10 * (i + 1))), i) for x in range(20)]
data.extend(q)
a.data = data
post.append([a, 'graphing-04'])
b = Graph3DBars(title='Random Data',
alpha=0.8,
barWidth=0.2,
doneAction=None,
colors=['b', 'r', 'g'])
b.data = data
post.append([b, 'graphing-05'])
for obj, name in post:
obj.process()
fn = name + '.png'
fp = str(environLocal.getRootTempDir() / fn)
environLocal.printDebug(['writing fp:', fp])
obj.write(fp)
def testColorGridLegend(self, doneAction=None):
from music21.analysis import discrete
ks = discrete.KrumhanslSchmuckler()
data = ks.solutionLegend()
# print(data)
a = GraphColorGridLegend(doneAction=doneAction, dpi=300)
a.data = data
a.process()
def testGraphVerticalBar(self):
g = GraphGroupedVerticalBar(doneAction=None)
data = [(f'bar{x}', {'a': 3, 'b': 2, 'c': 1}) for x in range(10)]
g.data = data
g.process()
if __name__ == '__main__':
import music21
music21.mainTest(Test) # , runTest='testPlot3DPitchSpaceQuarterLengthCount')