Files
pytheory/tests/test_diagrams.py
kennethreitz 87da8a2d0d feat: SVG fretboard diagrams — chord charts, scale shapes, arpeggio maps
New pytheory.diagrams renders fretboard data as clean, dependency-free
SVG you can embed in videos, slides, and worksheets:

- Fretboard.tab_image(name) / Fingering.to_svg() — vertical chord-box
  diagrams with open/muted markers, automatic barre detection, and the
  root highlighted in red.
- Fretboard.scale_shapes(scale) — splits a scale into positional boxes
  (the five pentatonic positions, etc.), each a small fret window with
  roots marked; .to_svg() per shape. Boxes follow the slant via a
  notes-per-string cap so they connect like real CAGED shapes.
- Fretboard.arpeggio_diagram(chord) — every chord tone across the neck,
  labelled by role (R/3/5/7…), roots highlighted.

SVG is pure text (no deps); pass fmt="png" to rasterize via the optional
cairosvg package.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 23:49:31 -04:00

146 lines
4.7 KiB
Python

import xml.dom.minidom as minidom
import pytest
from pytheory import Fretboard, TonedScale, Chord
from pytheory.diagrams import (chord_svg, scale_shapes, arpeggio_svg,
ScaleShape, _ROOT)
def _valid(svg):
minidom.parseString(svg) # raises on malformed XML
assert svg.startswith("<svg")
return svg
# ── chord diagrams ───────────────────────────────────────────────────
def test_chord_image_is_valid_svg():
svg = Fretboard.guitar().tab_image("Am")
_valid(svg)
assert "Am" in svg
def test_chord_image_marks_root_in_red():
# C major has two C roots (A string fret 3, B string fret 1).
svg = Fretboard.guitar().tab_image("C")
assert svg.count(_ROOT) == 2
def test_chord_image_detects_barre():
# F is a full barre — expect a rect (the bar) in the output.
assert "<rect" in Fretboard.guitar().tab_image("F").replace(
'<rect width', '') # ignore the background rect
def test_chord_image_writes_file(tmp_path):
out = tmp_path / "Am.svg"
ret = Fretboard.guitar().tab_image("Am", str(out))
assert ret == str(out)
assert out.read_text().startswith("<svg")
def test_fingering_to_svg():
svg = Fretboard.guitar().chord("G").to_svg("G")
_valid(svg)
def test_iterate_chord_images(tmp_path):
fb = Fretboard.guitar()
for name in ["C", "Am", "F", "G"]:
out = tmp_path / f"{name}.svg"
fb.tab_image(name, str(out))
assert out.exists()
# ── scale shapes ─────────────────────────────────────────────────────
def _a_minor_pentatonic():
return TonedScale(tonic="A4", system="blues")["minor pentatonic"]
def test_pentatonic_has_five_positions():
shapes = Fretboard.guitar().scale_shapes(_a_minor_pentatonic())
assert len(shapes) == 5
assert all(isinstance(s, ScaleShape) for s in shapes)
def test_pentatonic_two_notes_per_string():
"""Each pentatonic box is two notes on every string."""
from collections import Counter
for shape in Fretboard.guitar().scale_shapes(_a_minor_pentatonic()):
per_string = Counter(s for (s, _, _, _) in shape.notes)
assert set(per_string.values()) == {2}
def test_scale_shape_marks_roots():
shapes = Fretboard.guitar().scale_shapes(_a_minor_pentatonic())
# Every box contains at least one A root.
for shape in shapes:
assert any(is_root for (_, _, _, is_root) in shape.notes)
svg = shapes[0].to_svg()
assert _ROOT in svg
_valid(svg)
def test_scale_shape_window_is_small():
for shape in Fretboard.guitar().scale_shapes(_a_minor_pentatonic()):
lo, hi = shape.fret_range
assert hi - lo <= 4 # a comfortable hand span
def test_diatonic_three_notes_per_string():
from collections import Counter
cmaj = TonedScale(tonic="C4")["major"]
shapes = Fretboard.guitar().scale_shapes(cmaj)
assert len(shapes) == 7
counts = Counter(s for (s, _, _, _) in shapes[1].notes)
assert set(counts.values()) == {3}
def test_scale_shape_image_position_bounds():
fb = Fretboard.guitar()
scale = _a_minor_pentatonic()
_valid(fb.scale_shape_image(scale, 1))
with pytest.raises(ValueError):
fb.scale_shape_image(scale, 99)
# ── arpeggio diagrams ────────────────────────────────────────────────
def test_arpeggio_diagram_valid_and_labelled():
svg = Fretboard.guitar().arpeggio_diagram("Am")
_valid(svg)
assert ">R<" in svg # root label
assert ">5<" in svg # fifth label
def test_arpeggio_accepts_chord_object():
svg = Fretboard.guitar().arpeggio_diagram(Chord.from_symbol("Cmaj7"))
_valid(svg)
def test_arpeggio_note_labels():
svg = Fretboard.guitar().arpeggio_diagram("Am", labels="note")
assert ">A<" in svg and ">C<" in svg
def test_arpeggio_respects_max_fret():
narrow = Fretboard.guitar().arpeggio_diagram("Am", max_fret=5)
wide = Fretboard.guitar().arpeggio_diagram("Am", max_fret=12)
assert len(narrow) < len(wide)
# ── other instruments ────────────────────────────────────────────────
def test_works_on_ukulele():
svg = Fretboard.ukulele().tab_image("C")
_valid(svg)
def test_png_export_requires_cairosvg(tmp_path):
pytest.importorskip("cairosvg")
out = tmp_path / "Am.png"
Fretboard.guitar().tab_image("Am", str(out), fmt="png")
assert out.exists()