-
Notifications
You must be signed in to change notification settings - Fork 119
Description
In prototyping some viz code recently, I got to thinking that our generic handling of categorical labels in the display module could be better adapted for chord plots using the labeled segment display. This example, pulled from a librosa issue discussion from a while back librosa/librosa#1370 (comment), shows how the current behavior looks:
This is basically fine in that distinct categories get distinct colors, but there are a few significant drawbacks:
- There is no guaranteed consistency between plots that use different sets of labels. F#:maj is blue in the above, but it could be orange in a different track. This leads to some probably unnecessary context switching when viewing multiple annotations.
- There is no musical logic to the color organization - it's essentially down to the order in which the labels appear in the track.
- There is no notion of "color proximity". Hue and pitch class are both cyclic spaces (more or less), and this could be exploited to convey more information visually.
I started digging around the matplotlib options, and there really aren't any existing colormaps that would make much sense here. Seaborn actually provides some nice functionality here for hsl/husl colormap construction, but I don't think we want to add seaborn to the dependency stack here. Instead, I propose that we pre-generate a handful of custom colormaps for use in pitch-related plots.
Protoype colormaps
Since these are categorical colormaps, we really don't need to worry about things like equal luminosity or perceptual uniformity. What we do need to worry about is discriminability (under accessibility constraints, etc). For this reason, I'm starting with seaborn's hsl generator, using n=12 to get evenly spaced hues according to pitch class in chromatic order. I'm, generating light, medium, and dark versions of each, which I'm for now imagining as being used for other/major/minor qualities. (I'm not married to that particular idea, but it seems like a reasonable enough starting point.)
We can plot these colormaps in both chromatic and circle-of-fifths order, resulting in the following (chatbot-generated/vibecoded, but LGTM):
We can also do the reverse, generating in CoF order instead of chromatic order:
After chromatic<->cof translation, these essentially act like the tab10-style categorical maps present in matplotlib, maximally dispersing similar hues, except that we actually have 12 to work with.
Intended use
Color is a bit limited for what chord annotations convey, so there is always going to be a loss of information here. At present, this information loss is arbitrary, but I think a reasonable case can be made that we can prioritize the following concepts by importance: root note (hue), major/minor (3rd) quality (value), everything else (dim/aug/sus, sixths, sevenths, extensions). This motivates my proposal above for using the center ring palette for major-like qualities (ie maj, dom7, maj6 and the like), the inner ring (dark) for minor-like qualities (min, min7, etc), and the outer ring (light) for everything else.
The no-chord symbol (and, I guess, out-of-gamut X) would be represented as a neutral gray (center disc in the plots above).
In terms of which hsl sweep mode to use, I think either the chromatic or cof orderings can be justified, perhaps with different use cases. I rather like the cof sweep as it makes adjacent pitches (probably dissonant) look maximally distinct, but I'd like to hear thoughts from others on this. (To be clear, I think we can and should include both - this is really just a question of defaults.)
Example call signature
I envision the (default) user-facing code to look something like:
>>> mir_eval.display.chord(intervals, labels, major='medium', minor='dark', other='light', sweep='fifths')
So a user could opt to change the value shading for different qualities, or the domain of the hsl sweep, etc.
Notes
I did check these palettes with the WCAG accessibility checker under deuteranomaly and protanomaly, and they seem pretty discriminable to me. Probably the value fields could be optimized to minimize confusion between light/medium/dark rings, but overall I think it's a pretty solid start.
The one major drawback of the proposed idea is that distinct but similar chords would render as visually identical. So a region that alternates between "F#:maj" and "F#:7" would look like one solid region. Likewise, bass notes are essentially ignored, etc etc. Probably some of this could be massaged around by using fill patterns to provide more nuance than N/maj/min/other, but I do worry that the end result would end up looking like clown pants if not implemented carefully.