cmake build system: visiblity support for clang
[supercollider.git] / SCClassLibrary / Common / GUI / PlusGUI / Math / PlotView.sc
blob269d5003b13b642d29637edee5794c2519420913
1 Plot {
3         var <>plotter, <value, <>spec, <>domainSpec;
4         var <bounds, <plotBounds;
5         var <>font, <>fontColor, <>gridColorX, <>gridColorY, <>plotColor, <>backgroundColor;
6         var     <>gridLinePattern, <>gridLineSmoothing;
7         var <>gridOnX = true, <>gridOnY = true, <>labelX, <>labelY;
9         var valueCache;
11         *initClass {
12                 StartUp.add {
13                                 GUI.skin.put(\plot, (
14                                         gridColorX: Color.grey(0.7),
15                                         gridColorY: Color.grey(0.7),
16                                         fontColor: Color.grey(0.3),
17                                         plotColor: [Color.black, Color.blue, Color.red, Color.green(0.7)],
18                                         background: Color.new255(235, 235, 235),
19                                         gridLinePattern: nil,
20                                         gridLineSmoothing: false,
21                                         labelX: "",
22                                         labelY: "",
23                                         expertMode: false,
24                                         gridFont: Font( Font.defaultSansFace, 9 )
25                                 ));
26                 }
27         }
29         *new { |plotter|
30                 ^super.newCopyArgs(plotter).init
31         }
33         init {
34                 var skin = GUI.skin.at(\plot);
36                 skin.use {
37                         font = ~gridFont ?? { Font.default };
38                         if(font.class != GUI.font) { font = Font(font.name, font.size) };
39                         gridColorX = ~gridColorX;
40                         gridColorY = ~gridColorY;
41                         plotColor = ~plotColor;
42                         fontColor = ~fontColor;
43                         backgroundColor = ~background;
44                         gridLineSmoothing = ~gridLineSmoothing;
45                         gridLinePattern = ~gridLinePattern !? {~gridLinePattern.as(FloatArray)};
46                         labelX = ~labelX;
47                         labelY = ~labelY;
48                 };
49         }
51         bounds_ { |rect|
52                 var size = (try { "foo".bounds(font).height } ?? { font.size } * 1.5);
53                 plotBounds = if(rect.height > 40) { rect.insetBy(size, size) } { rect };
54                 bounds = rect;
55                 valueCache = nil;
56         }
58         value_ { |array|
59                 value = array;
60                 valueCache = nil;
61         }
64         draw {
65                 this.drawBackground;
66                 if(gridOnX) { this.drawGridX; this.drawNumbersX; };
67                 if(gridOnY) { this.drawGridY; this.drawNumbersY; };
68                 this.drawLabels;
69                 this.drawData;
70                 plotter.drawFunc.value(this); // additional elements
71         }
73         drawBackground {
74                 Pen.addRect(bounds);
75                 Pen.fillColor = backgroundColor;
76                 Pen.fill;
77         }
79         drawGridX {
81                 var top = plotBounds.top;
82                 var base = plotBounds.bottom;
84                 this.drawOnGridX { |hpos|
85                         Pen.moveTo(hpos @ base);
86                         Pen.lineTo(hpos @ top);
87                 };
89                 Pen.strokeColor = gridColorX;
90                 this.prStrokeGrid;
92         }
94         drawGridY {
96                 var left = plotBounds.left;
97                 var right = plotBounds.right;
99                 this.drawOnGridY { |vpos|
100                         Pen.moveTo(left @ vpos);
101                         Pen.lineTo(right @ vpos);
102                 };
104                 Pen.strokeColor = gridColorY;
105                 this.prStrokeGrid;
107         }
109         drawNumbersX {
110                 var top = plotBounds.top;
111                 var base = plotBounds.bottom;
112                 Pen.fillColor = fontColor;
113                 Pen.font = font;
114                 this.drawOnGridX { |hpos, val, i|
115                         var string = val.asStringPrec(5) ++ domainSpec.units;
116                         Pen.stringAtPoint(string, hpos @ base);
117                 };
118         }
120         drawNumbersY {
121                 var left = plotBounds.left;
122                 var right = plotBounds.right;
123                 Pen.fillColor = fontColor;
124                 Pen.font = font;
125                 this.drawOnGridY { |vpos, val, i|
126                         var string = val.asStringPrec(5).asString ++ spec.units;
127                         if(gridOnX.not or: { i > 0 }) {
128                                 Pen.stringAtPoint(string, left @ vpos);
129                         }
130                 };
131         }
134         drawOnGridX { |func|
136                 var width = plotBounds.width;
137                 var left = plotBounds.left;
138                 var n, gridValues;
139                 var xspec = domainSpec;
140                 if(this.hasSteplikeDisplay) {
141                         // special treatment of special case: lines need more space
142                         xspec = xspec.copy.maxval_(xspec.maxval * value.size / (value.size - 1))
143                 };
144                 n = (plotBounds.width / 64).round(2);
145                 if(xspec.hasZeroCrossing) { n = n + 1 };
147                 gridValues = xspec.gridValues(n);
148                 if(gridOnY) { gridValues = gridValues.drop(1) };
149                 gridValues = gridValues.drop(-1);
151                 gridValues.do { |val, i|
152                         var hpos = left + (xspec.unmap(val) * width);
153                         func.value(hpos, val, i);
154                 };
155         }
157         drawOnGridY { |func|
159                 var base = plotBounds.bottom;
160                 var height = plotBounds.height.neg; // measures from top left
161                 var n, gridValues;
163                 n = (plotBounds.height / 32).round(2);
164                 if(spec.hasZeroCrossing) { n = n + 1 };
165                 gridValues = spec.gridValues(n);
167                 gridValues.do { |val, i|
168                         var vpos = base + (spec.unmap(val) * height);
169                         func.value(vpos, val, i);
170                 };
172         }
174         drawLabels {
175                 var sbounds;
176                 if(gridOnX and: { labelX.notNil }) {
177                         sbounds = try { labelX.bounds(font) } ? 0;
178                         Pen.font = font;
179                         Pen.strokeColor = fontColor;
180                         Pen.stringAtPoint(labelX,
181                                 plotBounds.right - sbounds.width @ plotBounds.bottom
182                         )
183                 };
184                 if(gridOnY and: { labelY.notNil }) {
185                         sbounds = try { labelY.bounds(font) } ? 0;
186                         Pen.font = font;
187                         Pen.strokeColor = fontColor;
188                         Pen.stringAtPoint(labelY,
189                                 plotBounds.left - sbounds.width - 3 @ plotBounds.top
190                         )
191                 };
192         }
195         domainCoordinates { |size|
196                 var val = this.resampledDomainSpec.unmap(plotter.domain ?? { (0..size-1) });
197                 ^plotBounds.left + (val * plotBounds.width);
198         }
200         dataCoordinates {
201                  var val = spec.unmap(this.prResampValues);
202                  ^plotBounds.bottom - (val * plotBounds.height); // measures from top left (may be arrays)
203         }
205         resampledSize {
206                 ^min(value.size, plotBounds.width / plotter.resolution)
207         }
209         resampledDomainSpec {
210                 var offset = if(this.hasSteplikeDisplay) { 0 } { 1 };
211                 ^domainSpec.copy.maxval_(this.resampledSize - offset)
212         }
214         drawData {
216                 var mode = plotter.plotMode;
217                 var ycoord = this.dataCoordinates;
218                 var xcoord = this.domainCoordinates(ycoord.size);
220                 Pen.width = 1.0;
221                 Pen.joinStyle = 1;
222                 plotColor = plotColor.as(Array);
224                 if(ycoord.at(0).isSequenceableCollection) { // multi channel expansion
225                         ycoord.flop.do { |y, i|
226                                 Pen.beginPath;
227                                 this.perform(mode, xcoord, y);
228                                 Pen.strokeColor = plotColor.wrapAt(i);
229                                 Pen.stroke;
230                         }
231                 } {
232                         Pen.beginPath;
233                         Pen.strokeColor = plotColor.at(0);
234                         this.perform(mode, xcoord, ycoord);
235                         Pen.stroke;
236                 };
237                 Pen.joinStyle = 0;
239         }
241         // modes
243         linear { |x, y|
244                 Pen.moveTo(x.first @ y.first);
245                 y.size.do { |i|
246                         Pen.lineTo(x[i] @ y[i]);
247                 }
248         }
250         points { |x, y|
251                 var size = min(bounds.width / value.size * 0.25, 4);
252                 y.size.do { |i|
253                         Pen.addArc(x[i] @ y[i], 0.5, 0, 2pi);
254                         if(size > 2) { Pen.addArc(x[i] @ y[i], size, 0, 2pi); };
255                 }
256         }
258         plines { |x, y|
259                 var size = min(bounds.width / value.size * 0.25, 3);
260                 Pen.moveTo(x.first @ y.first);
261                 y.size.do { |i|
262                         var p = x[i] @ y[i];
263                         Pen.lineTo(p);
264                         Pen.addArc(p, size, 0, 2pi);
265                         Pen.moveTo(p);
266                 }
267         }
269         levels { |x, y|
270                 Pen.smoothing_(false);
271                 y.size.do { |i|
272                         Pen.moveTo(x[i] @ y[i]);
273                         Pen.lineTo(x[i + 1] ?? { plotBounds.right } @ y[i]);
274                 }
275         }
277         steps { |x, y|
278                 Pen.smoothing_(false);
279                 Pen.moveTo(x.first @ y.first);
280                 y.size.do { |i|
281                         Pen.lineTo(x[i] @ y[i]);
282                         Pen.lineTo(x[i + 1] ?? { plotBounds.right } @ y[i]);
283                 }
284         }
286         // editing
289         editDataIndex { |index, x, y, plotIndex|
290                 // WARNING: assuming index is in range!
291                 var val = this.getRelativePositionY(y);
292                 plotter.editFunc.value(plotter, plotIndex, index, val, x, y);
293                 value.put(index, val);
294                 valueCache = nil;
295         }
297         editData { |x, y, plotIndex|
298                 var index = this.getIndex(x);
299                 this.editDataIndex( index, x, y, plotIndex );
300         }
302         editDataLine { |pt1, pt2, plotIndex|
303                 var ptLeft, ptRight, ptLo, ptHi;
304                 var xSpec, ySpec;
305                 var i1, i2, iLo, iHi;
306                 var val;
308                 // get indexes related to ends of the line
309                 i1 = this.getIndex(pt1.x);
310                 i2 = this.getIndex(pt2.x);
312                 // if both ends at same index, simplify
313                 if( i1 == i2 ) {
314                         ^this.editDataIndex( i2, pt2.x, pt2.y, plotIndex );
315                 };
317                 // order points and indexes
318                 if( i1 < i2 ) {
319                         iLo = i1; iHi = i2;
320                         ptLeft = pt1; ptRight = pt2;
321                 }{
322                         iLo = i2; iHi = i1;
323                         ptLeft = pt2; ptRight = pt1;
324                 };
326                 // if same value all over, simplify
327                 if( ptLeft.y == ptRight.y ) {
328                         val = this.getRelativePositionY(ptLeft.y);
329                         while( {iLo <= iHi} ) {
330                                 value.put( iLo, val );
331                                 iLo = iLo + 1;
332                         };
333                         // trigger once for second end of the line
334                         plotter.editFunc.value(plotter, plotIndex, i2, val, pt2.x, pt2.y);
335                         valueCache = nil;
336                         ^this;
337                 };
339                 // get actual points corresponding to indexes
340                 xSpec = ControlSpec( ptLeft.x, ptRight.x );
341                 ySpec = ControlSpec( ptLeft.y, ptRight.y );
342                 ptLo = Point();
343                 ptHi = Point();
344                 ptLo.x = domainSpec.unmap(iLo) * plotBounds.width + plotBounds.left;
345                 ptHi.x = domainSpec.unmap(iHi) * plotBounds.width + plotBounds.left;
346                 ptLo.y = ySpec.map( xSpec.unmap(ptLo.x) );
347                 ptHi.y = ySpec.map( xSpec.unmap(ptHi.x) );
349                 // interpolate and store
350                 ySpec = ControlSpec( this.getRelativePositionY(ptLo.y), this.getRelativePositionY(ptHi.y) );
351                 xSpec = ControlSpec( iLo, iHi );
352                 while( {iLo <= iHi} ) {
353                         val = ySpec.map( xSpec.unmap(iLo) );
354                         value.put( iLo, val );
355                         iLo = iLo+1;
356                 };
358                 // trigger once for second end of the line
359                 plotter.editFunc.value(plotter, plotIndex, i2, val, pt2.x, pt2.y);
360                 valueCache = nil;
361         }
363         getRelativePositionX { |x|
364                 ^domainSpec.map((x - plotBounds.left) / plotBounds.width)
365         }
367         getRelativePositionY { |y|
368                 ^spec.map((plotBounds.bottom - y) / plotBounds.height)
369         }
371         hasSteplikeDisplay {
372                 ^#[\levels, \steps].includes(plotter.plotMode)
373         }
375         getIndex { |x|
376                 var offset = if(this.hasSteplikeDisplay) { 0.5 } { 0.0 }; // needs to be fixed.
377                 ^(this.getRelativePositionX(x) - offset).round.asInteger
378         }
380         getDataPoint { |x|
381                 var index = this.getIndex(x).clip(0, value.size - 1);
382                 ^[index, value.at(index)]
383         }
385         // settings
387         zoomFont { |val|
388                 font = font.copy;
389                 font.size = max(1, font.size + val);
390         }
393         // private implementation
395         prResampValues {
396                 ^if(value.size <= (plotBounds.width / plotter.resolution)) {
397                         value
398                 } {
399                         valueCache ?? { valueCache = value.resamp1(plotBounds.width / plotter.resolution) }
400                 }
401         }
403         prStrokeGrid {
404                 Pen.push;
406                 Pen.width = 1;
407                 try {
408                         Pen.smoothing_(gridLineSmoothing);
409                         if(gridLinePattern.notNil) {Pen.lineDash_(gridLinePattern)};
410                 };
411                 Pen.stroke;
413                 Pen.pop;
414         }
420 Plotter {
422         var <>name, <>bounds, <>parent;
423         var <value, <data, <>domain;
424         var <plots, <specs, <domainSpecs;
425         var <cursorPos, <>plotMode = \linear, <>editMode = false, <>normalized = false;
426         var <>resolution = 1, <>findSpecs = true, <superpose = false;
427         var modes, <interactionView;
428         var <editPlotIndex, <editPos;
430         var <>drawFunc, <>editFunc;
432         *new { |name, bounds, parent|
433                 ^super.newCopyArgs(name).makeWindow(parent, bounds)
434         }
436         makeWindow { |argParent, argBounds|
437                 parent = argParent ? parent;
438                 bounds = argBounds ? bounds;
439                 if(parent.isNil) {
440                         parent = Window.new(name ? "Plot", bounds ? Rect(100, 200, 400, 300));
441                         bounds = parent.view.bounds.insetBy(5, 0).moveBy(-5, 0);
442                         interactionView = UserView(parent, bounds);
443                         if(GUI.skin.at(\plot).at(\expertMode).not) { this.makeButtons };
444                         parent.drawFunc = { this.draw };
445                         parent.front;
446                         parent.onClose = { parent = nil };
448                 } {
449                         bounds = bounds ?? { parent.bounds.moveTo(0, 0) };
450                         interactionView = UserView(parent, bounds);
451                         interactionView.drawFunc = { this.draw };
453                 };
454                 modes = [\points, \levels, \linear, \plines, \steps].iter.loop;
456                 interactionView
457                         .background_(Color.clear)
458                         .focusColor_(Color.clear)
459                         .resize_(5)
460                         .focus(true)
461                         .mouseDownAction_({ |v, x, y, modifiers|
462                                 cursorPos = x @ y;
463                                 if(superpose.not) {
464                                         editPlotIndex = this.pointIsInWhichPlot(cursorPos);
465                                         editPlotIndex !? {
466                                                 editPos = x @ y; // new Point instead of cursorPos!
467                                                 if(editMode) {
468                                                         plots.at(editPlotIndex).editData(x, y, editPlotIndex);
469                                                         if(this.numFrames < 200) { this.refresh };
470                                                 };
471                                         }
472                                 };
473                                 if(modifiers.isAlt) { this.postCurrentValue(x, y) };
474                         })
475                         .mouseMoveAction_({ |v, x, y, modifiers|
476                                 cursorPos = x @ y;
477                                 if(superpose.not && editPlotIndex.notNil) {
478                                         if(editMode) {
479                                                 plots.at(editPlotIndex).editDataLine(editPos, cursorPos, editPlotIndex);
480                                                 if(this.numFrames < 200) { this.refresh };
481                                         };
482                                         editPos = x @ y;  // new Point instead of cursorPos!
483                                         
484                                 };
485                                 if(modifiers.isAlt) { this.postCurrentValue(x, y) };
486                         })
487                         .mouseUpAction_({
488                                 cursorPos = nil;
489                                 editPlotIndex = nil;
490                                 if(editMode && superpose.not) { this.refresh };
491                         })
492                         .keyDownAction_({ |view, char, modifiers, unicode, keycode|
493                                 if(modifiers.isCmd.not) {
494                                         switch(char,
495                                                 // y zoom out / font zoom
496                                                 $-, {
497                                                         if(modifiers.isCtrl) {
498                                                                 plots.do(_.zoomFont(-2));
499                                                         } {
500                                                                 this.specs = specs.collect(_.zoom(3/2));
501                                                                 normalized = false;
502                                                         }
503                                                 },
504                                                 // y zoom in / font zoom
505                                                 $+, {
506                                                         if(modifiers.isCtrl) {
507                                                                 plots.do(_.zoomFont(2));
508                                                         } {
509                                                                 this.specs = specs.collect(_.zoom(2/3));
510                                                                 normalized = false;
511                                                         }
512                                                 },
513                                                 // compare plots
514                                                 $=, {
515                                                         this.calcSpecs(separately: false);
516                                                         this.updatePlotSpecs;
517                                                         normalized = false;
518                                                 },
520                                                 /*// x zoom out (doesn't work yet)
521                                                 $*, {
522                                                         this.domainSpecs = domainSpecs.collect(_.zoom(3/2));
523                                                 },
524                                                 // x zoom in (doesn't work yet)
525                                                 $_, {
526                                                         this.domainSpecs = domainSpecs.collect(_.zoom(2/3))
527                                                 },*/
529                                                 // normalize
531                                                 $n, {
532                                                         if(normalized) {
533                                                                 this.specs = specs.collect(_.normalize)
534                                                         } {
535                                                                 this.calcSpecs;
536                                                                 this.updatePlotSpecs;
537                                                         };
538                                                         normalized = normalized.not;
539                                                 },
541                                                 // toggle grid
542                                                 $g, {
543                                                         plots.do { |x| x.gridOnY = x.gridOnY.not }
544                                                 },
545                                                 // toggle domain grid
546                                                 $G, {
547                                                         plots.do { |x| x.gridOnX = x.gridOnX.not };
548                                                 },
549                                                 // toggle plot mode
550                                                 $m, {
551                                                         this.plotMode = modes.next;
552                                                 },
553                                                 // toggle editing
554                                                 $e, {
555                                                         editMode = editMode.not;
556                                                         "plot edit mode %\n".postf(if(editMode) { "on" } { "off" });
557                                                 },
558                                                 // toggle superposition
559                                                 $s, {
560                                                         this.superpose = this.superpose.not;
561                                                 }
562                                         );
563                                         parent.refresh;
564                                 };
565                         });
566         }
568         makeButtons {
569                 var string = "?";
570                 var font = Font.sansSerif( 9 );
571                 var bounds = string.bounds(font);
572                 var padding = 8; // ensure that string is not clipped by round corners
574                 Button(parent, Rect(parent.view.bounds.right - 16, 8, bounds.width + padding, bounds.height + padding))
575                 .states_([["?"]])
576                 .focusColor_(Color.clear)
577                 .font_(font)
578                 .resize_(3)
579                 .action_ { this.class.openHelpFile };
580         }
583         value_ { |arrays|
584                 this.setValue(arrays, findSpecs, true)
585         }
587         setValue { |arrays, findSpecs = true, refresh = true|
588                 value = arrays;
589                 data = this.prReshape(arrays);
590                 if(findSpecs) {
591                                 this.calcSpecs;
592                                 this.calcDomainSpecs;
593                 };
594                 this.updatePlotSpecs;
595                 this.updatePlots;
596                 if(refresh) { this.refresh };
597         }
599         superpose_ { |flag|
600                 superpose = flag;
601                 if ( value.notNil ){
602                         this.setValue(value, false, true);
603                 };
604         }
606         numChannels {
607                 ^value.size
608         }
610         numFrames {
611                 if(value.isNil) { ^0 };
612                 ^value.first.size
613         }
615         draw {
616                 bounds = this.drawBounds;
617                 this.updatePlotBounds;
618                 Pen.use {
619                         plots.do { |plot| plot.draw };
620                 }
621         }
623         drawBounds {
624                 ^interactionView.bounds.insetBy(9, 8)
625         }
627         // subviews
629         updatePlotBounds {
630                 var deltaY = if(data.size > 1 ) { 4.0 } { 0.0 };
631                 var distY = bounds.height / data.size;
632                 var height = distY - deltaY;
634                 plots.do { |plot, i|
635                         plot.bounds_(
636                                 Rect(bounds.left, distY * i + bounds.top, bounds.width, height)
637                         )
638                 }
639         }
641         makePlots {
642                 var template = if(plots.isNil) { Plot(this) } { plots.last };
643                 plots !? { plots = plots.keep(data.size.neg) };
644                 plots = plots ++ template.dup(data.size - plots.size);
645                 plots.do { |plot, i| plot.value = data.at(i) };
647                 this.updatePlotSpecs;
648                 this.updatePlotBounds;
650         }
652         updatePlots {
653                 if(plots.size != data.size) {
654                         this.makePlots;
655                 } {
656                         plots.do { |plot, i|
657                                 plot.value = data.at(i)
658                         }
659                 }
660         }
662         updatePlotSpecs {
663                 specs !? {
664                         plots.do { |plot, i|
665                                 plot.spec = specs.clipAt(i)
666                         }
667                 };
668                 domainSpecs !? {
669                         plots.do { |plot, i|
670                                 plot.domainSpec = domainSpecs.clipAt(i)
671                         }
672                 }
673         }
675         setProperties { |... pairs|
676                 pairs.pairsDo { |selector, value|
677                         selector = selector.asSetter;
678                         plots.do { |x| x.perform(selector, value) }
679                 }
680         }
682         // specs
684         specs_ { |argSpecs|
685                 specs = argSpecs.asArray.clipExtend(data.size).collect(_.asSpec);
686                 this.updatePlotSpecs;
687         }
689         domainSpecs_ { |argSpecs|
690                 domainSpecs = argSpecs.asArray.clipExtend(data.size).collect(_.asSpec);
691                 this.updatePlotSpecs;
692         }
694         minval_ { |val|
695                 val = val.asArray;
696                 specs.do { |x, i| x.minval = val.wrapAt(i) };
697                 this.updatePlotSpecs;
698         }
700         maxval_ { |val|
701                 val = val.asArray;
702                 specs.do { |x, i| x.maxval = val.wrapAt(i) };
703                 this.updatePlotSpecs;
704         }
707         calcSpecs { |separately = true|
708                 specs = (specs ? [\unipolar.asSpec]).clipExtend(data.size);
709                 if(separately) {
710                         this.specs = specs.collect { |spec, i|
711                                 var list = data.at(i);
712                                 list !? { spec = spec.calcRange(list.flat).roundRange };
713                         }
714                 } {
715                         this.specs = specs.first.calcRange(data.flat).roundRange;
716                 }
717         }
720         calcDomainSpecs {
721                 // for now, a simple version
722                 domainSpecs = data.collect { |val|
723                                 [0, val.size - 1, \lin, 1].asSpec
724                 }
725         }
728         // interaction
731         pointIsInWhichPlot { |point|
732                 var res = plots.detectIndex { |plot|
733                         point.y.exclusivelyBetween(plot.bounds.top, plot.bounds.bottom)
734                 };
735                 ^res ?? {
736                                 if(point.y < bounds.center.y) { 0 } { plots.size - 1 }
737                 }
738         }
740         getDataPoint { |x, y|
741                 var plotIndex = this.pointIsInWhichPlot(x @ y);
742                 ^plotIndex !? {
743                                 plots.at(plotIndex).getDataPoint(x)
744                 }
745         }
747         postCurrentValue { |x, y|
748                 this.getDataPoint(x, y).postln
749         }
751         editData { |x, y|
752                 var plotIndex = this.pointIsInWhichPlot(x @ y);
753                 plotIndex !? {
754                                 plots.at(plotIndex).editData(x, y, plotIndex);
755                 };
756         }
758         refresh {
759                 parent !? { parent.refresh }
760         }
762         // private implementation
764         prReshape { |item|
765                 var size, array = item.asArray;
766                 if(item.first.isSequenceableCollection.not) {
767                         ^array.bubble;
768                 };
769                 if(superpose) {
770                         if(array.first.first.isSequenceableCollection) { ^array };
771                         size = array.maxItem { |x| x.size }.size;
772                         // for now, just extend data:
773                         ^array.collect { |x| x.asArray.clipExtend(size) }.flop.bubble           };
774                 ^array
775         }
783 // for now, use plot2.
786 + ArrayedCollection {
787         plot2 { |name, bounds, discrete=false, numChannels, minval, maxval|
788                 var array = this.as(Array), plotter = Plotter(name, bounds);
789                 if(discrete) { plotter.plotMode = \points };
791                 numChannels !? { array = array.unlace(numChannels) };
792                 array = array.collect {|elem|
793                         if (elem.isKindOf(Env)) {
794                                 elem.asSignal
795                         } {
796                                 elem
797                         }
798                 };
800                 plotter.setValue(array, true, false);
802                 minval !? { plotter.minval = minval; };
803                 maxval !? { plotter.maxval = maxval };
804                 plotter.refresh;
806                 ^plotter
807         }
810 + Collection {
812         plotHisto { arg steps = 100, min, max;
813                         var histo = this.histo(steps, min, max);
814                         var plotter = histo.plot2;
815                         plotter.domainSpecs = [[min ?? { this.minItem }, max ?? { this.maxItem }].asSpec];
816                         plotter.specs = [[0, histo.maxItem, \linear, 1].asSpec];
817                         plotter.plotMode = \steps;
818                         ^plotter
819         }
824 + Function {
826         plot2 { |duration = 0.01, server, bounds, minval, maxval|
827                 var name = this.asCompileString, plotter;
828                 if(name.size > 50 or: { name.includes(Char.nl) }) { name = "function plot" };
829                 plotter = [0].plot2(name, bounds);
830                 server = server ? Server.default;
831                 server.waitForBoot {
832                         this.loadToFloatArray(duration, server, { |array, buf|
833                                 var numChan = buf.numChannels;
834                                 {
835                                         plotter.value = array.unlace(buf.numChannels).collect(_.drop(-1));
836                                         plotter.domainSpecs = (ControlSpec(0, duration, units: "s"));
837                                         minval !? { plotter.minval = minval; };
838                                         maxval !? { plotter.maxval = maxval };
839                                         plotter.refresh;
841                                 }.defer
842                         })
843                 };
844                 ^plotter
845         }
849 + Wavetable {
851         plot2 { |name, bounds, minval, maxval|
852                 ^this.asSignal.plot2(name, bounds, minval: minval, maxval: maxval)
853         }
856 + Buffer {
858         plot2 { |name, bounds, minval, maxval|
859                 var plotter;
860                 if(server.serverRunning.not) { "Server % not running".format(server).warn; ^nil };
861                 plotter = [0].plot2(
862                         name ? "Buffer plot (bufnum: %)".format(this.bufnum),
863                         bounds, minval: minval, maxval: maxval
864                 );
865                 this.loadToFloatArray(action: { |array, buf|
866                         {
867                                 plotter.value = array.unlace(buf.numChannels);
868                                 plotter.setProperties(\labelX, "frames");
869                         }.defer
870                 });
871                 ^plotter
872         }
875 + Env {
877         plot2 { |size = 400, bounds, minval, maxval|
878                 var plotter = this.asSignal(size)
879                         .plot2("envelope plot", bounds, minval: minval, maxval: maxval);
880                 plotter.domainSpecs = ControlSpec(0, this.times.sum, units: "s");
881                 plotter.setProperties(\labelX, "time");
882                 plotter.refresh;
883                 ^plotter
884         }