import numpy as np
import pandas as pd
from plotnine import (
ggplot,
aes,
geom_point,
geom_path,
scale_x_continuous,
scale_y_continuous,
guides,
theme,
element_line,
element_rect,
)from mizani.transforms import trans
Guitar Neck ###
Using a transformed x-axis to visualise guitar chords
The x-axis is transformed to resemble the narrowing width of frets on a 25.5 inch Strat. To do that we create custom transformation.
The key parts of any transform object are the transform
and inverse
functions.
class frets_trans(trans):
"""
Frets Transformation
"""
= 23 # Including fret 0
number_of_frets = (0, number_of_frets - 1)
domain
@staticmethod
def transform(x):
= np.asarray(x)
x return 25.5 - (25.5 / (2 ** (x / 12)))
@staticmethod
def inverse(x):
= np.asarray(x)
x return 12 * np.log2(25.5 / (25.5 - x))
@classmethod
def breaks_(cls, limits):
# Fixed major breaks
return cls.domain
@classmethod
def minor_breaks(cls, major, limits):
# The major breaks as passed to this method are in transformed space.
# The minor breaks are calculated in data space to reveal the
# non-linearity of the scale.
= cls.inverse(major)
_major = cls.transform(np.linspace(*_major, cls.number_of_frets))
minor return minor
The above transform is different from most in that, breaks and minor breaks do not change. This is common of very specialized scales. It can also be a key requirement when creating graphics for demontration purposes.
Some chord Data
# Notes: the 0 fret is an open strum, all other frets are played half-way between fret bars.
# The strings are 1:low E, 2: A, 3: D, 4: G, 5: B, 6: E
= pd.DataFrame({"Fret": [0, 2.5, 1.5, 0, 0.5, 0], "String": [1, 2, 3, 4, 5, 6]})
c_chord
# Sequence based on the number of notes in the chord
"Sequence"] = list(range(1, 1 + len(c_chord["Fret"])))
c_chord[
# Standard markings for a Stratocaster
= pd.DataFrame(
markings
{"Fret": [2.5, 4.5, 6.5, 8.5, 11.5, 11.5, 14.5, 16.5, 18.5, 20.5],
"String": [3.5, 3.5, 3.5, 3.5, 2, 5, 3.5, 3.5, 3.5, 3.5],
} )
Visualizing the chord
# Gallery, elaborate
# Look and feel of the graphic
= "#FFDDCC"
neck_color = "#998888"
fret_color = "#AA9944"
string_color
= theme(
neck_theme =(10, 2),
figure_size=element_rect(fill=neck_color),
panel_background=element_line(color=string_color, size=2.2),
panel_grid_major_y=element_line(color=fret_color, size=2.2),
panel_grid_major_x=element_line(color=fret_color, size=1),
panel_grid_minor_x
)
("Fret", "String"))
ggplot(c_chord, aes(+ geom_path(aes(color="Sequence"), size=3)
+ geom_point(aes(color="Sequence"), fill="#FFFFFF", size=3)
+ geom_point(data=markings, fill="#000000", size=4)
+ scale_x_continuous(trans=frets_trans)
+ scale_y_continuous(breaks=range(0, 7), minor_breaks=[])
+ guides(color=False)
+ neck_theme
)
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) File ~/scm/python/plotnine/.venv/lib/python3.13/site-packages/IPython/core/formatters.py:984, in IPythonDisplayFormatter.__call__(self, obj) 982 method = get_real_method(obj, self.print_method) 983 if method is not None: --> 984 method() 985 return True File ~/scm/python/plotnine/plotnine/ggplot.py:149, in ggplot._ipython_display_(self) 142 def _ipython_display_(self): 143 """ 144 Display plot in the output of the cell 145 146 This method will always be called when a ggplot object is the 147 last in the cell. 148 """ --> 149 self._display() File ~/scm/python/plotnine/plotnine/ggplot.py:190, in ggplot._display(self) 187 self.theme = self.theme.to_retina() 189 buf = BytesIO() --> 190 self.save(buf, "png" if format == "retina" else format, verbose=False) 191 figure_size_px = self.theme._figure_size_px 192 display_func = get_display_function(format, figure_size_px) File ~/scm/python/plotnine/plotnine/ggplot.py:702, in ggplot.save(self, filename, format, path, width, height, units, dpi, limitsize, verbose, **kwargs) 653 def save( 654 self, 655 filename: Optional[str | Path | BytesIO] = None, (...) 664 **kwargs: Any, 665 ): 666 """ 667 Save a ggplot object as an image file 668 (...) 700 Additional arguments to pass to matplotlib `savefig()`. 701 """ --> 702 sv = self.save_helper( 703 filename=filename, 704 format=format, 705 path=path, 706 width=width, 707 height=height, 708 units=units, 709 dpi=dpi, 710 limitsize=limitsize, 711 verbose=verbose, 712 **kwargs, 713 ) 715 with plot_context(self).rc_context: 716 sv.figure.savefig(**sv.kwargs) File ~/scm/python/plotnine/plotnine/ggplot.py:650, in ggplot.save_helper(self, filename, format, path, width, height, units, dpi, limitsize, verbose, **kwargs) 647 if dpi is not None: 648 self.theme = self.theme + theme(dpi=dpi) --> 650 figure = self.draw(show=False) 651 return mpl_save_view(figure, fig_kwargs) File ~/scm/python/plotnine/plotnine/ggplot.py:322, in ggplot.draw(self, show) 319 self._create_figure() 320 figure = self.figure --> 322 self._build() 324 # setup 325 self.axs = self.facet.setup(self) File ~/scm/python/plotnine/plotnine/ggplot.py:423, in ggplot._build(self) 420 layers.map(npscales) 422 # Train coordinate system --> 423 layout.setup_panel_params(self.coordinates) 425 # fill in the defaults 426 layers.use_defaults_after_scale(scales) File ~/scm/python/plotnine/plotnine/facets/layout.py:198, in Layout.setup_panel_params(self, coord) 196 for i, j in self.layout[cols].itertuples(index=False): 197 i, j = i - 1, j - 1 --> 198 params = coord.setup_panel_params( 199 self.panel_scales_x[i], self.panel_scales_y[j] 200 ) 201 self.panel_params.append(params) File ~/scm/python/plotnine/plotnine/coords/coord_cartesian.py:88, in coord_cartesian.setup_panel_params(self, scale_x, scale_y) 84 sv = scale.view(limits=coord_limits, range=ranges.range) 85 return sv 87 out = panel_view( ---> 88 x=get_scale_view(scale_x, self.limits.x), 89 y=get_scale_view(scale_y, self.limits.y), 90 ) 91 return out File ~/scm/python/plotnine/plotnine/coords/coord_cartesian.py:84, in coord_cartesian.setup_panel_params.<locals>.get_scale_view(scale, limits) 80 expansion = scale.default_expansion(expand=self.expand) 81 ranges = scale.expand_limits( 82 scale.final_limits, expansion, coord_limits, identity_trans() 83 ) ---> 84 sv = scale.view(limits=coord_limits, range=ranges.range) 85 return sv File ~/scm/python/plotnine/plotnine/scales/scale_continuous.py:328, in scale_continuous.view(self, limits, range) 325 labels = self.get_labels(breaks) 327 ubreaks = self.get_breaks(range) --> 328 minor_breaks = self.get_minor_breaks(ubreaks, range) 330 sv = scale_view( 331 scale=self, 332 aesthetics=self.aesthetics, (...) 338 minor_breaks=minor_breaks, 339 ) 340 return sv File ~/scm/python/plotnine/plotnine/scales/scale_continuous.py:473, in scale_continuous.get_minor_breaks(self, major, limits) 471 minor_breaks = [] 472 elif self.minor_breaks is True: --> 473 minor_breaks: Sequence[float] = self._trans.minor_breaks( 474 major, limits 475 ) # pyright: ignore 476 elif isinstance(self.minor_breaks, int): 477 minor_breaks: Sequence[float] = self._trans.minor_breaks( 478 major, 479 limits, 480 self.minor_breaks, # pyright: ignore 481 ) Cell In[2], line 30, in frets_trans.minor_breaks(cls, major, limits) 24 @classmethod 25 def minor_breaks(cls, major, limits): 26 # The major breaks as passed to this method are in transformed space. 27 # The minor breaks are calculated in data space to reveal the 28 # non-linearity of the scale. 29 _major = cls.inverse(major) ---> 30 minor = cls.transform(np.linspace(*_major, cls.number_of_frets)) 31 return minor File ~/scm/python/plotnine/.venv/lib/python3.13/site-packages/numpy/_core/function_base.py:121, in linspace(start, stop, num, endpoint, retstep, dtype, axis, device) 25 @array_function_dispatch(_linspace_dispatcher) 26 def linspace(start, stop, num=50, endpoint=True, retstep=False, dtype=None, 27 axis=0, *, device=None): 28 """ 29 Return evenly spaced numbers over a specified interval. 30 (...) 119 120 """ --> 121 num = operator.index(num) 122 if num < 0: 123 raise ValueError( 124 "Number of samples, %s, must be non-negative." % num 125 ) TypeError: 'numpy.float64' object cannot be interpreted as an integer
<plotnine.ggplot.ggplot at 0x120e718c0>
Credit: This example was motivated by Jonathan Vitale who wanted to create graphics for a guitar scale trainer.