clean up indentation and spacing
[supercollider.git] / SCClassLibrary / Common / GUI / PlusGUI / Math / PlotView.sc
blob4900e46d00e8653cc3a3974f9c9fd31cccf5b644
1 Plot {
3         var <>plotter, <value;
4         var <bounds, <plotBounds,<>drawGrid;
6         var <spec, <domainSpec;
7         var <font, <fontColor, <gridColorX, <gridColorY, <>plotColor, <>backgroundColor;
8         var <gridOnX = true, <gridOnY = true, <>labelX, <>labelY;
10         var valueCache,pen;
12         *initClass {
13                 StartUp.add {
14                         GUI.skin.put(\plot, (
15                                 gridColorX: Color.grey(0.7),
16                                 gridColorY: Color.grey(0.7),
17                                 fontColor: Color.grey(0.3),
18                                 plotColor: [Color.black, Color.blue, Color.red, Color.green(0.7)],
19                                 background: Color.new255(235, 235, 235),
20                                 gridLinePattern: nil,
21                                 gridLineSmoothing: false,
22                                 labelX: "",
23                                 labelY: "",
24                                 expertMode: false,
25                                 gridFont: Font( Font.defaultSansFace, 9 )
26                         ));
27                 }
28         }
30         *new { |plotter|
31                 ^super.newCopyArgs(plotter).init
32         }
34         init {
35                 var fontName;
36                 var gui = plotter.gui;
37                 var skin = GUI.skin.at(\plot);
38                 pen = gui.pen;
40                 drawGrid = DrawGrid(bounds ? Rect(0,0,1,1),nil,nil);
41                 drawGrid.x.labelOffset = Point(0,4);
42                 drawGrid.y.labelOffset = Point(-10,0);
43                 skin.use {
44                         font = ~gridFont ?? { gui.font.default };
45                         if(font.class != gui.font) {
46                                 fontName = font.name;
47                                 if( gui.font.availableFonts.detect(_ == fontName).isNil, { fontName = gui.font.defaultSansFace });
48                                 font = gui.font.new(fontName, font.size)
49                         };
50                         this.gridColorX = ~gridColorX;
51                         this.gridColorY = ~gridColorY;
52                         plotColor = ~plotColor;
53                         this.fontColor = ~fontColor;
54                         backgroundColor = ~background;
55                         this.gridLineSmoothing = ~gridLineSmoothing;
56                         this.gridLinePattern = ~gridLinePattern !? {~gridLinePattern.as(FloatArray)};
57                         labelX = ~labelX;
58                         labelY = ~labelY;
59                 };
60         }
62         bounds_ { |rect|
63                 var size = (try { "foo".bounds(font).height } ?? { font.size } * 1.5);
64                 plotBounds = if(rect.height > 40) { rect.insetBy(size, size) } { rect };
65                 bounds = rect;
66                 valueCache = nil;
67                 drawGrid.bounds = plotBounds;
68         }
70         value_ { |array|
71                 value = array;
72                 valueCache = nil;
73         }
74         spec_ { |sp|
75                 spec = sp;
76                 if(gridOnY and: spec.notNil,{
77                         drawGrid.vertGrid = spec.grid;
78                 },{
79                         drawGrid.vertGrid = nil
80                 })
81         }
82         domainSpec_ { |sp|
83                 domainSpec = sp;
84                 if(gridOnX and: domainSpec.notNil,{
85                         drawGrid.horzGrid = domainSpec.grid;
86                 },{
87                         drawGrid.horzGrid = nil
88                 })
89         }
90         gridColorX_ { |c|
91                 drawGrid.x.gridColor = c;
92                 gridColorX = c;
93         }
94         gridColorY_ { |c|
95                 drawGrid.y.gridColor = c;
96                 gridColorY = c;
97         }
98         font_ { |f|
99                 font = f;
100                 drawGrid.font = f;
101         }
102         fontColor_ { |c|
103                 fontColor = c;
104                 drawGrid.fontColor = c;
105         }
106         gridLineSmoothing_ { |bool|
107                 drawGrid.smoothing = bool;
108         }
109         gridLinePattern_ { |pattern|
110                 drawGrid.linePattern = pattern;
111         }
112         gridOnX_ { |bool|
113                 gridOnX = bool;
114                 drawGrid.horzGrid = if(gridOnX,{domainSpec.grid},{nil});
115         }
116         gridOnY_ { |bool|
117                 gridOnY= bool;
118                 drawGrid.vertGrid = if(gridOnY,{spec.grid},{nil});
119         }
121         draw {
122                 this.drawBackground;
123                 drawGrid.draw;
124                 this.drawLabels;
125                 this.drawData;
126                 plotter.drawFunc.value(this); // additional elements
127         }
129         drawBackground {
130                 pen.addRect(bounds);
131                 pen.fillColor = backgroundColor;
132                 pen.fill;
133         }
135         drawLabels {
136                 var sbounds;
137                 if(gridOnX and: { labelX.notNil }) {
138                         sbounds = try { labelX.bounds(font) } ? 0;
139                         pen.font = font;
140                         pen.strokeColor = fontColor;
141                         pen.stringAtPoint(labelX,
142                                 plotBounds.right - sbounds.width @ plotBounds.bottom
143                         )
144                 };
145                 if(gridOnY and: { labelY.notNil }) {
146                         sbounds = try { labelY.bounds(font) } ? 0;
147                         pen.font = font;
148                         pen.strokeColor = fontColor;
149                         pen.stringAtPoint(labelY,
150                                 plotBounds.left - sbounds.width - 3 @ plotBounds.top
151                         )
152                 };
153         }
155         domainCoordinates { |size|
156                 var val = this.resampledDomainSpec.unmap(plotter.domain ?? { (0..size-1) });
157                 ^plotBounds.left + (val * plotBounds.width);
158         }
160         dataCoordinates {
161                  var val = spec.unmap(this.prResampValues);
162                  ^plotBounds.bottom - (val * plotBounds.height); // measures from top left (may be arrays)
163         }
165         resampledSize {
166                 ^min(value.size, plotBounds.width / plotter.resolution)
167         }
169         resampledDomainSpec {
170                 var offset = if(this.hasSteplikeDisplay) { 0 } { 1 };
171                 ^domainSpec.copy.maxval_(this.resampledSize - offset)
172         }
174         drawData {
175                 var mode = plotter.plotMode;
176                 var ycoord = this.dataCoordinates;
177                 var xcoord = this.domainCoordinates(ycoord.size);
179                 pen.width = 1.0;
180                 pen.joinStyle = 1;
181                 plotColor = plotColor.as(Array);
183                 if(ycoord.at(0).isSequenceableCollection) { // multi channel expansion
184                         ycoord.flop.do { |y, i|
185                                 pen.beginPath;
186                                 this.perform(mode, xcoord, y);
187                                 pen.strokeColor = plotColor.wrapAt(i);
188                                 pen.stroke;
189                         }
190                 } {
191                         pen.beginPath;
192                         pen.strokeColor = plotColor.at(0);
193                         this.perform(mode, xcoord, ycoord);
194                         pen.stroke;
195                 };
196                 pen.joinStyle = 0;
198         }
200         // modes
202         linear { |x, y|
203                 pen.moveTo(x.first @ y.first);
204                 y.size.do { |i|
205                         pen.lineTo(x[i] @ y[i]);
206                 }
207         }
209         points { |x, y|
210                 var size = min(bounds.width / value.size * 0.25, 4);
211                 y.size.do { |i|
212                         pen.addArc(x[i] @ y[i], 0.5, 0, 2pi);
213                         if(size > 2) { pen.addArc(x[i] @ y[i], size, 0, 2pi); };
214                 }
215         }
217         plines { |x, y|
218                 var size = min(bounds.width / value.size * 0.25, 3);
219                 pen.moveTo(x.first @ y.first);
220                 y.size.do { |i|
221                         var p = x[i] @ y[i];
222                         pen.lineTo(p);
223                         pen.addArc(p, size, 0, 2pi);
224                         pen.moveTo(p);
225                 }
226         }
228         levels { |x, y|
229                 pen.smoothing_(false);
230                 y.size.do { |i|
231                         pen.moveTo(x[i] @ y[i]);
232                         pen.lineTo(x[i + 1] ?? { plotBounds.right } @ y[i]);
233                 }
234         }
236         steps { |x, y|
237                 pen.smoothing_(false);
238                 pen.moveTo(x.first @ y.first);
239                 y.size.do { |i|
240                         pen.lineTo(x[i] @ y[i]);
241                         pen.lineTo(x[i + 1] ?? { plotBounds.right } @ y[i]);
242                 }
243         }
245         // editing
248         editDataIndex { |index, x, y, plotIndex|
249                 // WARNING: assuming index is in range!
250                 var val = this.getRelativePositionY(y);
251                 plotter.editFunc.value(plotter, plotIndex, index, val, x, y);
252                 value.put(index, val);
253                 valueCache = nil;
254         }
256         editData { |x, y, plotIndex|
257                 var index = this.getIndex(x);
258                 this.editDataIndex( index, x, y, plotIndex );
259         }
261         editDataLine { |pt1, pt2, plotIndex|
262                 var ptLeft, ptRight, ptLo, ptHi;
263                 var xSpec, ySpec;
264                 var i1, i2, iLo, iHi;
265                 var val;
267                 // get indexes related to ends of the line
268                 i1 = this.getIndex(pt1.x);
269                 i2 = this.getIndex(pt2.x);
271                 // if both ends at same index, simplify
272                 if( i1 == i2 ) {
273                         ^this.editDataIndex( i2, pt2.x, pt2.y, plotIndex );
274                 };
276                 // order points and indexes
277                 if( i1 < i2 ) {
278                         iLo = i1; iHi = i2;
279                         ptLeft = pt1; ptRight = pt2;
280                 }{
281                         iLo = i2; iHi = i1;
282                         ptLeft = pt2; ptRight = pt1;
283                 };
285                 // if same value all over, simplify
286                 if( ptLeft.y == ptRight.y ) {
287                         val = this.getRelativePositionY(ptLeft.y);
288                         while( {iLo <= iHi} ) {
289                                 value.put( iLo, val );
290                                 iLo = iLo + 1;
291                         };
292                         // trigger once for second end of the line
293                         plotter.editFunc.value(plotter, plotIndex, i2, val, pt2.x, pt2.y);
294                         valueCache = nil;
295                         ^this;
296                 };
298                 // get actual points corresponding to indexes
299                 xSpec = ControlSpec( ptLeft.x, ptRight.x );
300                 ySpec = ControlSpec( ptLeft.y, ptRight.y );
301                 ptLo = Point();
302                 ptHi = Point();
303                 ptLo.x = domainSpec.unmap(iLo) * plotBounds.width + plotBounds.left;
304                 ptHi.x = domainSpec.unmap(iHi) * plotBounds.width + plotBounds.left;
305                 ptLo.y = ySpec.map( xSpec.unmap(ptLo.x) );
306                 ptHi.y = ySpec.map( xSpec.unmap(ptHi.x) );
308                 // interpolate and store
309                 ySpec = ControlSpec( this.getRelativePositionY(ptLo.y), this.getRelativePositionY(ptHi.y) );
310                 xSpec = ControlSpec( iLo, iHi );
311                 while( {iLo <= iHi} ) {
312                         val = ySpec.map( xSpec.unmap(iLo) );
313                         value.put( iLo, val );
314                         iLo = iLo+1;
315                 };
317                 // trigger once for second end of the line
318                 plotter.editFunc.value(plotter, plotIndex, i2, val, pt2.x, pt2.y);
319                 valueCache = nil;
320         }
322         getRelativePositionX { |x|
323                 ^domainSpec.map((x - plotBounds.left) / plotBounds.width)
324         }
326         getRelativePositionY { |y|
327                 ^spec.map((plotBounds.bottom - y) / plotBounds.height)
328         }
330         hasSteplikeDisplay {
331                 ^#[\levels, \steps].includes(plotter.plotMode)
332         }
334         getIndex { |x|
335                 var offset = if(this.hasSteplikeDisplay) { 0.5 } { 0.0 }; // needs to be fixed.
336                 ^(this.getRelativePositionX(x) - offset).round.asInteger
337         }
339         getDataPoint { |x|
340                 var index = this.getIndex(x).clip(0, value.size - 1);
341                 ^[index, value.at(index)]
342         }
344         // settings
346         zoomFont { |val|
347                 font = font.copy;
348                 font.size = max(1, font.size + val);
349                 this.font = font;
350                 drawGrid.clearCache;
351         }
352         copy {
353                 ^super.copy.drawGrid_(drawGrid.copy)
354         }
355         prResampValues {
356                 ^if(value.size <= (plotBounds.width / plotter.resolution)) {
357                         value
358                 } {
359                         valueCache ?? { valueCache = value.resamp1(plotBounds.width / plotter.resolution) }
360                 }
361         }
366 Plotter {
368         var <>name, <>bounds, <>parent;
369         var <value, <data, <>domain;
370         var <plots, <specs, <domainSpecs;
371         var <cursorPos, <>plotMode = \linear, <>editMode = false, <>normalized = false;
372         var <>resolution = 1, <>findSpecs = true, <superpose = false;
373         var modes, <interactionView;
374         var <editPlotIndex, <editPos;
376         var <>drawFunc, <>editFunc;
377         var <gui;
379         *new { |name, bounds, parent|
380                 ^super.newCopyArgs(name).makeWindow(parent, bounds)
381         }
383         makeWindow { |argParent, argBounds|
384                 parent = argParent ? parent;
385                 bounds = argBounds ? bounds;
386                 gui = GUI.current;
387                 if(parent.isNil) {
388                         parent = gui.window.new(name ? "Plot", bounds ? Rect(100, 200, 400, 300));
389                         bounds = parent.view.bounds.insetBy(5, 0).moveBy(-5, 0);
390                         interactionView = gui.userView.new(parent, bounds);
391                         if(GUI.skin.at(\plot).at(\expertMode).not) { this.makeButtons };
392                         parent.drawFunc = { this.draw };
393                         parent.front;
394                         parent.onClose = { parent = nil };
396                 } {
397                         bounds = bounds ?? { parent.bounds.moveTo(0, 0) };
398                         interactionView = gui.userView.new(parent, bounds);
399                         interactionView.drawFunc = { this.draw };
400                 };
401                 modes = [\points, \levels, \linear, \plines, \steps].iter.loop;
403                 interactionView
404                         .background_(Color.clear)
405                         .focusColor_(Color.clear)
406                         .resize_(5)
407                         .focus(true)
408                         .mouseDownAction_({ |v, x, y, modifiers|
409                                 cursorPos = x @ y;
410                                 if(superpose.not) {
411                                         editPlotIndex = this.pointIsInWhichPlot(cursorPos);
412                                         editPlotIndex !? {
413                                                 editPos = x @ y; // new Point instead of cursorPos!
414                                                 if(editMode) {
415                                                         plots.at(editPlotIndex).editData(x, y, editPlotIndex);
416                                                         if(this.numFrames < 200) { this.refresh };
417                                                 };
418                                         }
419                                 };
420                                 if(modifiers.isAlt) { this.postCurrentValue(x, y) };
421                         })
422                         .mouseMoveAction_({ |v, x, y, modifiers|
423                                 cursorPos = x @ y;
424                                 if(superpose.not && editPlotIndex.notNil) {
425                                         if(editMode) {
426                                                 plots.at(editPlotIndex).editDataLine(editPos, cursorPos, editPlotIndex);
427                                                 if(this.numFrames < 200) { this.refresh };
428                                         };
429                                         editPos = x @ y;  // new Point instead of cursorPos!
431                                 };
432                                 if(modifiers.isAlt) { this.postCurrentValue(x, y) };
433                         })
434                         .mouseUpAction_({
435                                 cursorPos = nil;
436                                 editPlotIndex = nil;
437                                 if(editMode && superpose.not) { this.refresh };
438                         })
439                         .keyDownAction_({ |view, char, modifiers, unicode, keycode|
440                                 if(modifiers.isCmd.not) {
441                                         switch(char,
442                                                 // y zoom out / font zoom
443                                                 $-, {
444                                                         if(modifiers.isCtrl) {
445                                                                 plots.do(_.zoomFont(-2));
446                                                         } {
447                                                                 this.specs = specs.collect(_.zoom(3/2));
448                                                                 normalized = false;
449                                                         }
450                                                 },
451                                                 // y zoom in / font zoom
452                                                 $+, {
453                                                         if(modifiers.isCtrl) {
454                                                                 plots.do(_.zoomFont(2));
455                                                         } {
456                                                                 this.specs = specs.collect(_.zoom(2/3));
457                                                                 normalized = false;
458                                                         }
459                                                 },
460                                                 // compare plots
461                                                 $=, {
462                                                         this.calcSpecs(separately: false);
463                                                         this.updatePlotSpecs;
464                                                         normalized = false;
465                                                 },
467                                                 /*// x zoom out (doesn't work yet)
468                                                 $*, {
469                                                         this.domainSpecs = domainSpecs.collect(_.zoom(3/2));
470                                                 },
471                                                 // x zoom in (doesn't work yet)
472                                                 $_, {
473                                                         this.domainSpecs = domainSpecs.collect(_.zoom(2/3))
474                                                 },*/
476                                                 // normalize
478                                                 $n, {
479                                                         if(normalized) {
480                                                                 this.specs = specs.collect(_.normalize)
481                                                         } {
482                                                                 this.calcSpecs;
483                                                                 this.updatePlotSpecs;
484                                                         };
485                                                         normalized = normalized.not;
486                                                 },
488                                                 // toggle grid
489                                                 $g, {
490                                                         plots.do { |x| x.gridOnY = x.gridOnY.not }
491                                                 },
492                                                 // toggle domain grid
493                                                 $G, {
494                                                         plots.do { |x| x.gridOnX = x.gridOnX.not };
495                                                 },
496                                                 // toggle plot mode
497                                                 $m, {
498                                                         this.plotMode = modes.next;
499                                                 },
500                                                 // toggle editing
501                                                 $e, {
502                                                         editMode = editMode.not;
503                                                         "plot edit mode %\n".postf(if(editMode) { "on" } { "off" });
504                                                 },
505                                                 // toggle superposition
506                                                 $s, {
507                                                         this.superpose = this.superpose.not;
508                                                 }
509                                         );
510                                         parent.refresh;
511                                 };
512                         });
513         }
515         makeButtons {
516                 var string = "?";
517                 var font = gui.font.sansSerif( 9 );
518                 var bounds = string.bounds(font);
519                 var padding = 8; // ensure that string is not clipped by round corners
521                 gui.button.new(parent, Rect(parent.view.bounds.right - 16, 8, bounds.width + padding, bounds.height + padding))
522                 .states_([["?"]])
523                 .focusColor_(Color.clear)
524                 .font_(font)
525                 .resize_(3)
526                 .action_ { this.class.openHelpFile };
527         }
530         value_ { |arrays|
531                 this.setValue(arrays, findSpecs, true)
532         }
534         setValue { |arrays, findSpecs = true, refresh = true|
535                 value = arrays;
536                 data = this.prReshape(arrays);
537                 if(findSpecs) {
538                         this.calcSpecs;
539                         this.calcDomainSpecs;
540                 };
541                 this.updatePlotSpecs;
542                 this.updatePlots;
543                 if(refresh) { this.refresh };
544         }
546         superpose_ { |flag|
547                 superpose = flag;
548                 if ( value.notNil ){
549                         this.setValue(value, false, true);
550                 };
551         }
553         numChannels {
554                 ^value.size
555         }
557         numFrames {
558                 if(value.isNil) { ^0 };
559                 ^value.first.size
560         }
562         draw {
563                 var b;
564                 b = this.drawBounds;
565                 if(b != bounds) {
566                         bounds = b;
567                         this.updatePlotBounds;
568                 };
569                 gui.pen.use {
570                         plots.do { |plot| plot.draw };
571                 }
572         }
574         drawBounds {
575                 ^interactionView.bounds.insetBy(9, 8)
576         }
578         // subviews
580         updatePlotBounds {
581                 var deltaY = if(data.size > 1 ) { 4.0 } { 0.0 };
582                 var distY = bounds.height / data.size;
583                 var height = distY - deltaY;
585                 plots.do { |plot, i|
586                         plot.bounds_(
587                                 Rect(bounds.left, distY * i + bounds.top, bounds.width, height)
588                         )
589                 }
590         }
592         makePlots {
593                 var template = if(plots.isNil) { Plot(this) } { plots.last };
594                 plots !? { plots = plots.keep(data.size.neg) };
595                 plots = plots ++ template.dup(data.size - plots.size);
596                 plots.do { |plot, i| plot.value = data.at(i) };
598                 this.updatePlotSpecs;
599                 this.updatePlotBounds;
600         }
602         updatePlots {
603                 if(plots.size != data.size) {
604                         this.makePlots;
605                 } {
606                         plots.do { |plot, i|
607                                 plot.value = data.at(i)
608                         }
609                 }
610         }
612         updatePlotSpecs {
613                 specs !? {
614                         plots.do { |plot, i|
615                                 plot.spec = specs.clipAt(i)
616                         }
617                 };
618                 domainSpecs !? {
619                         plots.do { |plot, i|
620                                 plot.domainSpec = domainSpecs.clipAt(i)
621                         }
622                 }
623         }
625         setProperties { |... pairs|
626                 pairs.pairsDo { |selector, value|
627                         selector = selector.asSetter;
628                         plots.do { |x| x.perform(selector, value) }
629                 }
630         }
632         // specs
634         specs_ { |argSpecs|
635                 specs = argSpecs.asArray.clipExtend(data.size).collect(_.asSpec);
636                 this.updatePlotSpecs;
637         }
639         domainSpecs_ { |argSpecs|
640                 domainSpecs = argSpecs.asArray.clipExtend(data.size).collect(_.asSpec);
641                 this.updatePlotSpecs;
642         }
644         minval_ { |val|
645                 val = val.asArray;
646                 specs.do { |x, i| x.minval = val.wrapAt(i) };
647                 this.updatePlotSpecs;
648         }
650         maxval_ { |val|
651                 val = val.asArray;
652                 specs.do { |x, i| x.maxval = val.wrapAt(i) };
653                 this.updatePlotSpecs;
654         }
657         calcSpecs { |separately = true|
658                 specs = (specs ? [\unipolar.asSpec]).clipExtend(data.size);
659                 if(separately) {
660                         this.specs = specs.collect { |spec, i|
661                                 var list = data.at(i);
662                                 list !? { spec = spec.looseRange(list.flat) };
663                         }
664                 } {
665                         this.specs = specs.first.looseRange(data.flat);
666                 }
667         }
669         calcDomainSpecs {
670                 // for now, a simple version
671                 domainSpecs = data.collect { |val|
672                         [0, val.size - 1, \lin, 1].asSpec
673                 }
674         }
677         // interaction
678         pointIsInWhichPlot { |point|
679                 var res = plots.detectIndex { |plot|
680                         point.y.exclusivelyBetween(plot.bounds.top, plot.bounds.bottom)
681                 };
682                 ^res ?? {
683                         if(point.y < bounds.center.y) { 0 } { plots.size - 1 }
684                 }
685         }
687         getDataPoint { |x, y|
688                 var plotIndex = this.pointIsInWhichPlot(x @ y);
689                 ^plotIndex !? {
690                                 plots.at(plotIndex).getDataPoint(x)
691                 }
692         }
694         postCurrentValue { |x, y|
695                 this.getDataPoint(x, y).postln
696         }
698         editData { |x, y|
699                 var plotIndex = this.pointIsInWhichPlot(x @ y);
700                 plotIndex !? {
701                                 plots.at(plotIndex).editData(x, y, plotIndex);
702                 };
703         }
705         refresh {
706                 parent !? { parent.refresh }
707         }
709         prReshape { |item|
710                 var size, array = item.asArray;
711                 if(item.first.isSequenceableCollection.not) {
712                         ^array.bubble;
713                 };
714                 if(superpose) {
715                         if(array.first.first.isSequenceableCollection) { ^array };
716                         size = array.maxItem { |x| x.size }.size;
717                         // for now, just extend data:
718                         ^array.collect { |x| x.asArray.clipExtend(size) }.flop.bubble           };
719                 ^array
720         }
724 + ArrayedCollection {
725         plot { |name, bounds, discrete=false, numChannels, minval, maxval|
726                 var array = this.as(Array), plotter = Plotter(name, bounds);
727                 if(discrete) { plotter.plotMode = \points };
729                 numChannels !? { array = array.unlace(numChannels) };
730                 array = array.collect {|elem|
731                         if (elem.isKindOf(Env)) {
732                                 elem.asSignal
733                         } {
734                                 elem
735                         }
736                 };
737                 plotter.setValue(array, true, false);
738                 if(minval.notNil and: {maxval.notNil},{
739                         plotter.specs = [minval,maxval].asSpec
740                 },{
741                         minval !? { plotter.minval = minval; };
742                         maxval !? { plotter.maxval = maxval };
743                 });
744                 plotter.refresh;
745                 ^plotter
746         }
749 + Collection {
750         plotHisto { arg steps = 100, min, max;
751                 var histo = this.histo(steps, min, max);
752                 var plotter = histo.plot;
753                 plotter.domainSpecs = [[min ?? { this.minItem }, max ?? { this.maxItem }].asSpec];
754                 plotter.specs = [[0, histo.maxItem, \linear, 1].asSpec];
755                 plotter.plotMode = \steps;
756                 ^plotter
757         }
761 + Function {
762         loadToFloatArray { arg duration = 0.01, server, action;
763                 var buffer, def, synth, name, numChannels, val, rate;
764                 server = server ? Server.default;
765                 if(server.serverRunning.not) { "Server not running!".warn; ^nil };
767                 name = this.hash.asString;
768                 def = SynthDef(name, { |bufnum|
769                         var     val = this.value;
770                         if(val.isValidUGenInput.not) {
771                                 val.dump;
772                                 Error("loadToFloatArray failed: % is no valid UGen input".format(val)).throw
773                         };
774                         val = UGen.replaceZeroesWithSilence(val.asArray);
775                         rate = val.rate;
776                         if(rate == \audio) { // convert mixed rate outputs:
777                                 val = val.collect { |x| if(x.rate != \audio) { K2A.ar(x) } { x } }
778                         };
779                         if(val.size == 0) { numChannels = 1 } { numChannels = val.size };
780                         RecordBuf.perform(RecordBuf.methodSelectorForRate(rate), val, bufnum, loop:0);
781                         Line.perform(Line.methodSelectorForRate(rate), dur: duration, doneAction: 2);
782                 });
784                 Routine.run({
785                         var c, numFrames;
786                         c = Condition.new;
787                         numFrames = duration * server.sampleRate;
788                         if(rate == \control) { numFrames = numFrames / server.options.blockSize };
789                         buffer = Buffer.new(server, numFrames, numChannels);
790                         server.sendMsgSync(c, *buffer.allocMsg);
791                         server.sendMsgSync(c, "/d_recv", def.asBytes);
792                         synth = Synth(name, [\bufnum, buffer], server);
793                         OSCpathResponder(server.addr, ['/n_end', synth.nodeID], {
794                                 buffer.loadToFloatArray(action: { |array, buf|
795                                         action.value(array, buf);
796                                         buffer.free;
797                                         server.sendMsg("/d_free", name);
798                                 });
799                         }).add.removeWhenDone;
800                 });
801         }
803         plot { |duration = 0.01, server, bounds, minval, maxval|
804                 var name = this.asCompileString, plotter;
805                 if(name.size > 50 or: { name.includes(Char.nl) }) { name = "function plot" };
806                 plotter = [0].plot(name, bounds);
807                 server = server ? Server.default;
808                 server.waitForBoot {
809                         this.loadToFloatArray(duration, server, { |array, buf|
810                                 var numChan = buf.numChannels;
811                                 {
812                                         plotter.domainSpecs = ControlSpec(0, duration, units: "s");
813                                         minval !? { plotter.minval = minval; };
814                                         maxval !? { plotter.maxval = maxval };
815                                         plotter.setValue(array.unlace(buf.numChannels).collect(_.drop(-1)),false,true);
817                                 }.defer
818                         })
819                 };
820                 ^plotter
821         }
824 + Wavetable {
825         plot { |name, bounds, minval, maxval|
826                 ^this.asSignal.plot(name, bounds, minval: minval, maxval: maxval)
827         }
830 + Buffer {
831         plot { |name, bounds, minval, maxval|
832                 var plotter;
833                 if(server.serverRunning.not) { "Server % not running".format(server).warn; ^nil };
834                 plotter = [0].plot(
835                         name ? "Buffer plot (bufnum: %)".format(this.bufnum),
836                         bounds, minval: minval, maxval: maxval
837                 );
838                 this.loadToFloatArray(action: { |array, buf|
839                         {
840                                 if(minval.notNil and: {maxval.notNil},{
841                                         plotter.specs = [minval,maxval].asSpec
842                                 },{
843                                         minval !? { plotter.minval = minval; };
844                                         maxval !? { plotter.maxval = maxval };
845                                 });
846                                 plotter.domainSpecs = ControlSpec(0.0,buf.numFrames,units:"frames");
847                                 plotter.setValue(array.unlace(buf.numChannels),false,true);
848                         }.defer
849                 });
850                 ^plotter
851         }
854 + Env {
855         plot { |size = 400, bounds, minval, maxval|
856                 var plotter = this.asSignal(size)
857                         .plot("envelope plot", bounds, minval: minval, maxval: maxval);
858                 plotter.domainSpecs = ControlSpec(0, this.times.sum, units: "s");
859                 plotter.setProperties(\labelX, "time");
860                 plotter.refresh;
861                 ^plotter
862         }
865 + AbstractFunction {
866         plotGraph { arg n=500, from = 0.0, to = 1.0, name, bounds, discrete = false,
867                                 numChannels, minval, maxval, parent, labels = true;
868                 var array = Array.interpolation(n, from, to);
869                 var res = array.collect { |x| this.value(x) };
870                 res.plot(name, bounds, discrete, numChannels, minval, maxval, parent, labels)
871         }