class:: Plotter
summary:: Plot numerical data on a window or view
categories:: GUI>Accessories
related:: Reference/plot

description::
Plot data of up to three dimensions on a link::Classes/Window:: or link::Classes/UserView::.

subsection:: Keyboard shortcuts

When the plotter window has focus, the following keyboard shortcuts can be used to change the display:
table::
## + / - || vertical zoom
## = || compare plot channels
## n || toggle normalize display (0..1) / (-1..1), or fit range
## s || toggle superposition (see: superpose)
## m || switch plot mode (see: link::Classes/Plotter#-plotMode::)
## e || toggle editing (see: link::Classes/Plotter#-editMode::)
## g || toggle horizontal (domain) grid
## G || toggle vertical (codomain) grid
## p || print curve
## ctrl-+ / - || zoom font
## alt-click || post value
::

subsection:: Method extensions
Plotter extends other classes with methods. To see what classes implements plot, see link::Overviews/Methods#plot::

method:: plot (args)

code::
// plot array
[1, 6, 2, -5, 2].plot;
(0..100).normalize(0, 8pi).sin.plot;

// nested arrays
{ (0..100).normalize(0, 15.0.rand).sin }.dup(3).plot;
{ { (0..17).normalize(0, 15.0.rand).sin }.dup(4) }.dup(3).plot;

// UGen functions
{ SinOsc.ar([700, 357]) * SinOsc.ar([400, 476]) * 0.2 }.plot;

// Buffer
Buffer.read(s, Platform.resourceDir +/+ "sounds/SinedPink.aiff").plot;

// Env
Env.perc(0.4, 0.6).plot;
::

method:: plotGraph (n,from,to,...)

code::
{ |x| sin(x) }.plotGraph(300,0,2*pi);
{ |x| sin(1/x)*x }.plotGraph(from:0.0001,to:0.2);
::

section:: Changing global defaults

The default styles are kept (and may be overridden) in code::GUI.skin.at(\plot)::. See also link::Classes/GUI:: help.

code::
// specify plot layout
(
GUI.skin.plot.gridLinePattern = FloatArray[1, 0];
GUI.skin.plot.fontColor = Color(0.5, 1, 0);
GUI.skin.plot.gridColorX = Color.yellow(0.5);
GUI.skin.plot.gridColorY = Color.yellow(0.5);
GUI.skin.plot.background = Color.black;
GUI.skin.plot.plotColor = (10..0).normalize(0.1, 1).collect { |i| Color.rand(i) };
GUI.skin.plot.labelX = "X";
GUI.skin.plot.labelY = "Y";
);

(
x = { |i| (0..60).scramble.clump(8) * (3.5 ** i) }.dup(3);
x.plot("ARRAY:PLOT", Rect(200, 300, 600, 500));
)

GUI.skin.plot.put(\plotColor, { Color.rand(0.0, 0.8) } ! 8);
[(0..100), (20..120), (40..140)].squared.flop.bubble.plot;

// reset the defaults:
Plot.initClass;
::

classmethods::

method:: new
argument:: name
Plot window title.
argument:: bounds
The window bounds (a link::Classes/Rect::).
argument:: parent
Either a link::Classes/Window:: / link::Classes/View:: may be passed in - then the plot is embedded. Otherwise a new link::Classes/Window:: is created.
discussion::
code::
(
a = Plotter("the plot", Rect(600, 30, 800, 250));
a.value = (0..1000).normalize(0, 14pi).curdle(0.01).scramble.flat.sin;
)
::

instancemethods::

subsection:: Accessing Instance Variables

method:: makeWindow
Open given plotter in a new window or within a given composite view.
argument:: argParent
Either a link::Classes/Window:: or link::Classes/View:: may be passed in - then the plot is embedded. Otherwise a new link::Classes/Window:: is created.
argument:: argBounds
The window bounds (a link::Classes/Rect::).

method:: plotMode
Get/Set the style of data display. This can be an array of different modes for
multi-channel data.
argument:: modes
A link::Classes/Symbol:: or an link::Classes/Array:: of link::Classes/Symbol::s.

If code::modes.size < numChannels::, the plots will emphasis::wrap:: around the
array of modes.
returns::
An link::Classes/Array:: of link::Classes/Symbol::s,
unless there is only one mode specified or if all modes of a multi-channel plot
are the same, in which case a link::Classes/Symbol:: is returned.

Available modes:
table::
## code::\linear:: || connecting data points with linear interpolation
## code::\points:: || draw data points only
## code::\plines:: || combination of lines and points
## code::\levels:: || horizontal lines
## code::\steps:: || connecting data points with step interpolation
## code::\bars:: || bar graph with filled bars
::
discussion::
code::
a = (0..20).scramble.plot;
a.plotMode = \points; a.refresh;
a.plotMode = \plines; a.refresh;
a.plotMode = \levels; a.refresh;
a.plotMode = \steps; a.refresh;
a.plotMode = \linear; a.refresh;
a.plotMode = \bars; a.refresh;

/* mixed modes */
(
c = [Color.red, Color.cyan, Color.green, Color.blue];
d = 4.collect({ |i| 100.collect({ |j| sinPi(1+i.squared*j * 50.reciprocal) })});
p = d.plot;
p.plotColor_(c);
p.plotMode_([\steps, \linear, \plines, \points]);
// superpose will trigger a refresh to update colors/modes
p.superpose_(true);
)
::

method:: setProperties
Set properties of all plot views. Defaults are taken from code::GUI.skin.at(\plot);::
argument:: ... pairs
A list of symbol,value pairs. Supported properties:
list::
## font
## fontColor
## gridColorX
## gridColorY
## plotColor (an link::Classes/Array::)
## backgroundColor
## gridLinePattern
## gridLineSmoothing ( link::Classes/Boolean:: )
## labelX
## labelY
## gridOnX ( link::Classes/Boolean:: )
## gridOnY ( link::Classes/Boolean:: )
::
discussion::
Example:
code::
(
a = { (0..30).scramble }.dup(2).plot;
a.setProperties(
	\fontColor, Color.red,
	\plotColor, Color.blue,
	\backgroundColor, Color.black,
	\gridColorX, Color.white,
	\labelX, "Humidity"
);
a.refresh;
);

GUI.skin.at(\plot); // defaults
::

method:: editMode
If the edit mode is set to true, the data may be edited via cursor.
code::
a = (0..20).plot;
a.editMode = true; // now edit the data by clicking into the plot..
a.value; // the value
::

method:: resolution
Set the minimum number of pixels between data points (default: 1)
code::
// Changing plot resolution (x-axis)
p = 5000.collect({ |i| sinPi(i * 0.0025) }).plot;
p.resolution_(5).refresh;  // 5px
p.resolution_(10).refresh;
p.resolution_(50).refresh; // undersampled
p.resolution_(1).refresh;  // default
::

method:: findSpecs
If true (default: code::true::), specs are derived from new data (using min and max values) automatically.

method::superpose
If set to true, plotter displays channels on top of each other (keyboard shortcut: s)
code::
a = { (0..30).scramble }.dup(2).plot;
a.superpose = true; a.refresh;
::

method:: value
Return or set the data values. Data may be numerical arrays of up to 3 dimensions.
code::
a = [1, 4, 2, 7, 4].dup(2).plot;
a.value;
::

method::setValue
Set the data values, with additional arguments determining how the plot is updated.
argument:: arrays
Arrays of data to plot. Data may be numerical arrays of up to 3 dimensions.
argument:: findSpecs
A code::Boolean::. If code::true:: (default), bounds of the plot(s) are determined automatically.
If code::false::, previous bounds will persist.
argument:: refresh
A code::Boolean::. If code::true:: (default), refresh the view immediately.
argument:: separately
A code::Boolean::. If code::true:: (default), min and max of each set of data will be calculated
and displayed separately for each plot. If code::false::, each plot's range on the
y-axis will be the same.
argument::minval
(Optional) The minimum value displayed on the y-axis.
argument::maxval
(Optional) The maximum value displayed on the y-axis.
argument:: defaultRange
(Optional) A default range for the y-axis in the case
that the max and min values of the data are identical.


method:: data
Reference to the current internal data.

method:: cursorPos
Returns:: the last cursorPos (a link::Classes/Point::).

method:: plots
Returns:: the single subplots (a link::Classes/Plot::).


method:: specs
Set or get the spec for the y-axis (codomain).
code::
a = { (40..3000).scramble }.dup(2).plot;
a.specs = \freq.asSpec; a.refresh;
::
See also link::#examples::.

method:: domainSpecs
Set or get the spec for the x-axis (domain).
code::
a = { (40..300).scramble }.dup(2).plot;
a.domainSpecs = \freq.asSpec; a.refresh;
::

discussion::
The default domainSpec when setting your Plotter link::#-value:: is a linear
spec in the range code::[0, value.size-1]::. Unless you set link::#-domain::,
your values are displayed as evenly sampled between the code::minval:: and
code::maxval:: of the domainSpecs.

If a new link::#-value:: is set, you will need to update your domainSpecs.

For examples, see link::#-domain:: and link::#examples::.

method:: domain
Set or get the x-axis positions of your data points. The size of the
code::domainArray:: must equal the size of your value array, i.e. a domain
value specified for each data point.

discussion::
Domain values are mapped into the range of the link::#-domainSpecs::, so need
not be evenly distributed. If link::#-domain:: is set to code::nil::, your
values are displayed as evenly sampled between the code::minval:: and
code::maxval:: of the link::#-domainSpecs::.

Currently, for multichannel data plots, it's assumed that all channels of data
share a single link::#-domain::. I.e. code::domainArray:: must be an
link::Classes/Array:: of rank 1.


If a new link::#-value:: is set, you will need to update your link::#-domain::.

Example:
code::
(
var data, domain, plot;
domain = 25.collect({rrand(0, 50)}).sort;
data = domain.linlin(0, 50, 0, 1);

plot = data.plot.plotMode_(\points);
plot.domainSpecs_([0, 50].asSpec);
plot.domain_(domain);
)
::
A more elaborate example can be found in the link::#examples::.

method:: plotColor
Set or get the colors of your data plot.

argument:: colors
This can be an link::Classes/Array:: of link::Classes/Color::s for
multichannel data, or a single link::Classes/Color:: to map to all channels.

If code::colors.size < numChannels::, the plots will emphasis::wrap:: around the
array of colors.
returns::
An link::Classes/Array:: of link::Classes/Color::s, unless there is only one
color specified or if all colors of a multi-channel plot are the same, in which
case a link::Classes/Color:: is returned.

method:: editFunc
Supply a function which is evaluated when editing data. The function is called with the arguments: code::plotter::, code::plotIndex::, code::index::, code::val::, code::x::, code::y::.
discussion::
Example:
code::
(
a = { (0..10).scramble.normalize }.dup(2).plot;
a.editMode = true;
a.editFunc = { |...args| args.postln };
);

// using plotter as a control interface
(
a = (0..10).scramble.normalize(300, 400).plot;
a.specs = \freq; a.plotMode = \points;
a.editMode = true;
x = { SinOsc.ar(\freq.kr(a.value)).mean * 0.1 }.play;
a.editFunc = { |plotter, plotIndex, i, val|
	x.setn(\freq, a.value)
};
a.parent.onClose = { x.release };
);

(
a = { (0..10).scramble.normalize(300, 400) }.dup.plot;
a.specs = \freq; a.plotMode = \bars;
a.editMode = true;
x = {
	var phase = SinOsc.ar(\rate.kr(a.value[1]));
	SinOsc.ar(\freq.kr(a.value[0]), phase).mean * 0.1
}.play;
a.editFunc = { |plotter, plotIndex, i, val|
	x.setn(\freq, a.value[0]);
	x.setn(\rate, a.value[1]);
};
a.parent.onClose = { x.release };
);
::

private:: plotColors

examples::

code::
// embedding in another GUI
(
w = Window("plot panel", Rect(20, 30, 520, 450));
Slider.new(w, Rect(10, 10, 490, 20)).resize_(2).action_ { |v|
    a.value = (0..(v.value.linexp(0, 1, 5, 8000)).asInteger).scramble;
    w.refresh;
};
z = CompositeView(w, Rect(10, 35, 490, 400)).background_(Color.rand(0.7)).resize_(5);
a = Plotter("plot", parent: z).value_([0, 1, 2, 3, 4].scramble * 100);
w.front;
)


(
a = Plotter("the plot", Rect(600, 30, 600, 400));
a.value = (0..100).normalize(0, 8pi).sin;
)

a.value = { |i| (0..90) % (i + 12) + ( (0..90) % (i + 2 * 1) ) }.dup(3);
a.value = (0..12).squared;
a.plotMode = \points; a.refresh;
a.plotMode = \levels; a.refresh;
a.plotMode = \plines; a.refresh;

a.domainSpecs = [[0, 115, \lin, 1]]; a.refresh;

a.parent.close; // close window
a.makeWindow;	// open it again

a.value = { (0..70).scramble }.dup(3);
a.plotMode = \linear; a.refresh;
a.value = { |i| (0..2000).normalize(0, 4pi + i).sin } ! 4; // lots of values, test efficiency
a.value = { |i| (0..10000).normalize(0, 8pi + i).sin } ! 3; // lots of values, test efficiency
a.value = { (0..140).scramble } ! 7;

a.value = { |i| (0..90).normalize(0, 8pi + (i*2pi)).sin } ! 2 * [400, 560] + 700;
a.value = { |i| (_ + 2.0.rand).dup(100).normalize(0, 8pi + i).sin } ! 2 * 400 + 700;


// multi channel expansion of single values
a.value = { |i| (_ + 2.0.rand).dup(100).normalize(0, 8pi + i).sin *.t [1, 2, 3] } ! 2 * 400 + 700;
a.value = { |i| (0..10) **.t [1, 1.2, 1.3, 1.5] * (3.5 ** i) }.dup(3);

a.parent.bounds = Rect(400, 100, 500, 700);
a.parent.bounds = Rect(600, 30, 500, 300);

a.superpose = true;
a.value = { |i| (0..20) * (3.5 ** i) }.dup(5);
a.superpose = false;

// specs

a.value = (50..90).midicps.scramble;
a.specs = \freq; a.refresh;
a.value = (1..60).scramble.neg;
a.specs = \db; a.refresh;

a.value = { |i| { exprand(1e3, (10 ** (i + 8))) }.dup(90) }.dup(3);
a.value = { { exprand(1e3, 1e9) }.dup(90) }.dup(3);
a.specs = [[1e3, 1e10, \exp], [1e3, 1e20, \exp], [1e3, 1e30, \exp]]; a.refresh;
a.domainSpecs = [[0, 5], [-8, 100], [-1, 1]]; a.refresh;


// Array:plot
(
a = (4 ** (-5..0)).postln.plot;
a.specs = \delay; a.refresh;
a.domainSpecs = [0, 10, \lin, 0, 0, " Kg"].asSpec; a.refresh;
);

a.domainSpecs = [0.1, 10, \exponential, 0, 0, " Kg"].asSpec; a.refresh;
a.domainSpecs = [-10, 10, \lin, 0, 0, " Kg"].asSpec; a.refresh;


a = [(0..100) * 9, (200..1300) * 2, (200..1000)/ 5].plot;
a.superpose = true;

a = [[0, 1.2, 1.5], [0, 1.3, 1.5, 1.6], [0, 1.5, 1.8, 2, 6]].midiratio.plot;
a.plotMode = \levels; a.refresh;
a.superpose = false;


// Function:plot
a = { SinOsc.ar([700, 357]) * SinOsc.ar([400, 476]) * 0.2 }.plot;
a = { SinOsc.ar([700, 357] *0.02) * SinOsc.ar([400, 476]) * 0.3 }.plot(0.2, minval: -1);
a = { SinOsc.ar(440) }.plot(1);


// Env:plot
Env.perc(0.4, 0.6).plot;
Env.new({ 1.0.rand2 }! 8, { 1.0.rand } ! 7, \sin).plot;

// Buffer:plot
b = Buffer.read(s, Platform.resourceDir +/+ "sounds/SinedPink.aiff");
		// Platform.resourceDir +/+ "sounds/SinedPink.aiff" contains SinOsc on left, PinkNoise on right
b.plot;
b.free;

// Setting the domain, run the full block:
(
var ys, xs; // ys: data, xs: domain positions
var rs = 0, plt2;

// xs are the sample points over a 2pi range

// sample locations, normalized
xs = 50.collect({rrand(0.05, 1)}).normalizeSum;
xs.do{ |me, i| xs[i] = me + rs; rs = rs + me; };

// force beginning/end to land on 0/2pi
xs[0] = 0;
xs[xs.size-1] = 1;

// scale these points to a 2pi range
xs = xs * 2pi;

// ys are the values of the sinusoid at the sampled points
ys = sin(xs);

// the uneven sampling is apparent when
// displayed as uniformly sampled
ys.plot("original data plot, uniform domain", [100, 100, 385, 285].asRect);

// generate a second plot whose domainSpec is specified:
// we see a smooth sinusoid cycle beginning
// at 0 and ending at 2pi
plt2 = ys.plot("resampled domain", [100+400, 100, 385, 285].asRect);

// domain values place plotted points where they
// actually sampled the sinusoid, thus visually
// reconstructing it.
plt2.domain = xs;

// these values are placed within the domainSpec
// so we can pad the plot on either side.
plt2.domainSpecs_(([-0.5pi, 2.5pi]).asSpec);
)

// Non-linear scaling of axes.
// plot 1000 values, linearly distributed on X and Y
p = (1, 2 .. 1000).plot.domainSpecs_([1, 1000, \lin].asSpec).refresh;
// scale y axis exponentially
p.specs_([[1, 1000, \exp].asSpec]).refresh;
// then scale x axis exponentially
p.domainSpecs_([1, 1000, \exp].asSpec).refresh;
::
