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;
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),
20 gridLineSmoothing: false,
24 gridFont: Font( Font.defaultSansFace, 9 )
30 ^super.newCopyArgs(plotter).init
34 var skin = GUI.skin.at(\plot);
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)};
52 var size = (try { "foo".bounds(font).height } ?? { font.size } * 1.5);
53 plotBounds = if(rect.height > 40) { rect.insetBy(size, size) } { rect };
66 if(gridOnX) { this.drawGridX; this.drawNumbersX; };
67 if(gridOnY) { this.drawGridY; this.drawNumbersY; };
70 plotter.drawFunc.value(this); // additional elements
75 Pen.fillColor = backgroundColor;
81 var top = plotBounds.top;
82 var base = plotBounds.bottom;
84 this.drawOnGridX { |hpos|
85 Pen.moveTo(hpos @ base);
86 Pen.lineTo(hpos @ top);
89 Pen.strokeColor = gridColorX;
96 var left = plotBounds.left;
97 var right = plotBounds.right;
99 this.drawOnGridY { |vpos|
100 Pen.moveTo(left @ vpos);
101 Pen.lineTo(right @ vpos);
104 Pen.strokeColor = gridColorY;
110 var top = plotBounds.top;
111 var base = plotBounds.bottom;
112 Pen.fillColor = fontColor;
114 this.drawOnGridX { |hpos, val, i|
115 var string = val.asStringPrec(5) ++ domainSpec.units;
116 Pen.stringAtPoint(string, hpos @ base);
121 var left = plotBounds.left;
122 var right = plotBounds.right;
123 Pen.fillColor = fontColor;
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);
136 var width = plotBounds.width;
137 var left = plotBounds.left;
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))
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);
159 var base = plotBounds.bottom;
160 var height = plotBounds.height.neg; // measures from top left
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);
176 if(gridOnX and: { labelX.notNil }) {
177 sbounds = try { labelX.bounds(font) } ? 0;
179 Pen.strokeColor = fontColor;
180 Pen.stringAtPoint(labelX,
181 plotBounds.right - sbounds.width @ plotBounds.bottom
184 if(gridOnY and: { labelY.notNil }) {
185 sbounds = try { labelY.bounds(font) } ? 0;
187 Pen.strokeColor = fontColor;
188 Pen.stringAtPoint(labelY,
189 plotBounds.left - sbounds.width - 3 @ plotBounds.top
195 domainCoordinates { |size|
196 var val = this.resampledDomainSpec.unmap(plotter.domain ?? { (0..size-1) });
197 ^plotBounds.left + (val * plotBounds.width);
201 var val = spec.unmap(this.prResampValues);
202 ^plotBounds.bottom - (val * plotBounds.height); // measures from top left (may be arrays)
206 ^min(value.size, plotBounds.width / plotter.resolution)
209 resampledDomainSpec {
210 var offset = if(this.hasSteplikeDisplay) { 0 } { 1 };
211 ^domainSpec.copy.maxval_(this.resampledSize - offset)
216 var mode = plotter.plotMode;
217 var ycoord = this.dataCoordinates;
218 var xcoord = this.domainCoordinates(ycoord.size);
222 plotColor = plotColor.as(Array);
224 if(ycoord.at(0).isSequenceableCollection) { // multi channel expansion
225 ycoord.flop.do { |y, i|
227 this.perform(mode, xcoord, y);
228 Pen.strokeColor = plotColor.wrapAt(i);
233 Pen.strokeColor = plotColor.at(0);
234 this.perform(mode, xcoord, ycoord);
244 Pen.moveTo(x.first @ y.first);
246 Pen.lineTo(x[i] @ y[i]);
251 var size = min(bounds.width / value.size * 0.25, 4);
253 Pen.addArc(x[i] @ y[i], 0.5, 0, 2pi);
254 if(size > 2) { Pen.addArc(x[i] @ y[i], size, 0, 2pi); };
259 var size = min(bounds.width / value.size * 0.25, 3);
260 Pen.moveTo(x.first @ y.first);
264 Pen.addArc(p, size, 0, 2pi);
270 Pen.smoothing_(false);
272 Pen.moveTo(x[i] @ y[i]);
273 Pen.lineTo(x[i + 1] ?? { plotBounds.right } @ y[i]);
278 Pen.smoothing_(false);
279 Pen.moveTo(x.first @ y.first);
281 Pen.lineTo(x[i] @ y[i]);
282 Pen.lineTo(x[i + 1] ?? { plotBounds.right } @ y[i]);
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);
297 editData { |x, y, plotIndex|
298 var index = this.getIndex(x);
299 this.editDataIndex( index, x, y, plotIndex );
302 editDataLine { |pt1, pt2, plotIndex|
303 var ptLeft, ptRight, ptLo, ptHi;
305 var i1, i2, iLo, iHi;
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
314 ^this.editDataIndex( i2, pt2.x, pt2.y, plotIndex );
317 // order points and indexes
320 ptLeft = pt1; ptRight = pt2;
323 ptLeft = pt2; ptRight = pt1;
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 );
333 // trigger once for second end of the line
334 plotter.editFunc.value(plotter, plotIndex, i2, val, pt2.x, pt2.y);
339 // get actual points corresponding to indexes
340 xSpec = ControlSpec( ptLeft.x, ptRight.x );
341 ySpec = ControlSpec( ptLeft.y, ptRight.y );
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 );
358 // trigger once for second end of the line
359 plotter.editFunc.value(plotter, plotIndex, i2, val, pt2.x, pt2.y);
363 getRelativePositionX { |x|
364 ^domainSpec.map((x - plotBounds.left) / plotBounds.width)
367 getRelativePositionY { |y|
368 ^spec.map((plotBounds.bottom - y) / plotBounds.height)
372 ^#[\levels, \steps].includes(plotter.plotMode)
376 var offset = if(this.hasSteplikeDisplay) { 0.5 } { 0.0 }; // needs to be fixed.
377 ^(this.getRelativePositionX(x) - offset).round.asInteger
381 var index = this.getIndex(x).clip(0, value.size - 1);
382 ^[index, value.at(index)]
389 font.size = max(1, font.size + val);
393 // private implementation
396 ^if(value.size <= (plotBounds.width / plotter.resolution)) {
399 valueCache ?? { valueCache = value.resamp1(plotBounds.width / plotter.resolution) }
408 Pen.smoothing_(gridLineSmoothing);
409 if(gridLinePattern.notNil) {Pen.lineDash_(gridLinePattern)};
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)
436 makeWindow { |argParent, argBounds|
437 parent = argParent ? parent;
438 bounds = argBounds ? bounds;
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 };
446 parent.onClose = { parent = nil };
449 bounds = bounds ?? { parent.bounds.moveTo(0, 0) };
450 interactionView = UserView(parent, bounds);
451 interactionView.drawFunc = { this.draw };
454 modes = [\points, \levels, \linear, \plines, \steps].iter.loop;
457 .background_(Color.clear)
458 .focusColor_(Color.clear)
461 .mouseDownAction_({ |v, x, y, modifiers|
464 editPlotIndex = this.pointIsInWhichPlot(cursorPos);
466 editPos = x @ y; // new Point instead of cursorPos!
468 plots.at(editPlotIndex).editData(x, y, editPlotIndex);
469 if(this.numFrames < 200) { this.refresh };
473 if(modifiers.isAlt) { this.postCurrentValue(x, y) };
475 .mouseMoveAction_({ |v, x, y, modifiers|
477 if(superpose.not && editPlotIndex.notNil) {
479 plots.at(editPlotIndex).editDataLine(editPos, cursorPos, editPlotIndex);
480 if(this.numFrames < 200) { this.refresh };
482 editPos = x @ y; // new Point instead of cursorPos!
485 if(modifiers.isAlt) { this.postCurrentValue(x, y) };
490 if(editMode && superpose.not) { this.refresh };
492 .keyDownAction_({ |view, char, modifiers, unicode, keycode|
493 if(modifiers.isCmd.not) {
495 // y zoom out / font zoom
497 if(modifiers.isCtrl) {
498 plots.do(_.zoomFont(-2));
500 this.specs = specs.collect(_.zoom(3/2));
504 // y zoom in / font zoom
506 if(modifiers.isCtrl) {
507 plots.do(_.zoomFont(2));
509 this.specs = specs.collect(_.zoom(2/3));
515 this.calcSpecs(separately: false);
516 this.updatePlotSpecs;
520 /*// x zoom out (doesn't work yet)
522 this.domainSpecs = domainSpecs.collect(_.zoom(3/2));
524 // x zoom in (doesn't work yet)
526 this.domainSpecs = domainSpecs.collect(_.zoom(2/3))
533 this.specs = specs.collect(_.normalize)
536 this.updatePlotSpecs;
538 normalized = normalized.not;
543 plots.do { |x| x.gridOnY = x.gridOnY.not }
545 // toggle domain grid
547 plots.do { |x| x.gridOnX = x.gridOnX.not };
551 this.plotMode = modes.next;
555 editMode = editMode.not;
556 "plot edit mode %\n".postf(if(editMode) { "on" } { "off" });
558 // toggle superposition
560 this.superpose = this.superpose.not;
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))
576 .focusColor_(Color.clear)
579 .action_ { this.class.openHelpFile };
584 this.setValue(arrays, findSpecs, true)
587 setValue { |arrays, findSpecs = true, refresh = true|
589 data = this.prReshape(arrays);
592 this.calcDomainSpecs;
594 this.updatePlotSpecs;
596 if(refresh) { this.refresh };
602 this.setValue(value, false, true);
611 if(value.isNil) { ^0 };
616 bounds = this.drawBounds;
617 this.updatePlotBounds;
619 plots.do { |plot| plot.draw };
624 ^interactionView.bounds.insetBy(9, 8)
630 var deltaY = if(data.size > 1 ) { 4.0 } { 0.0 };
631 var distY = bounds.height / data.size;
632 var height = distY - deltaY;
636 Rect(bounds.left, distY * i + bounds.top, bounds.width, height)
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;
653 if(plots.size != data.size) {
657 plot.value = data.at(i)
665 plot.spec = specs.clipAt(i)
670 plot.domainSpec = domainSpecs.clipAt(i)
675 setProperties { |... pairs|
676 pairs.pairsDo { |selector, value|
677 selector = selector.asSetter;
678 plots.do { |x| x.perform(selector, value) }
685 specs = argSpecs.asArray.clipExtend(data.size).collect(_.asSpec);
686 this.updatePlotSpecs;
689 domainSpecs_ { |argSpecs|
690 domainSpecs = argSpecs.asArray.clipExtend(data.size).collect(_.asSpec);
691 this.updatePlotSpecs;
696 specs.do { |x, i| x.minval = val.wrapAt(i) };
697 this.updatePlotSpecs;
702 specs.do { |x, i| x.maxval = val.wrapAt(i) };
703 this.updatePlotSpecs;
707 calcSpecs { |separately = true|
708 specs = (specs ? [\unipolar.asSpec]).clipExtend(data.size);
710 this.specs = specs.collect { |spec, i|
711 var list = data.at(i);
712 list !? { spec = spec.calcRange(list.flat).roundRange };
715 this.specs = specs.first.calcRange(data.flat).roundRange;
721 // for now, a simple version
722 domainSpecs = data.collect { |val|
723 [0, val.size - 1, \lin, 1].asSpec
731 pointIsInWhichPlot { |point|
732 var res = plots.detectIndex { |plot|
733 point.y.exclusivelyBetween(plot.bounds.top, plot.bounds.bottom)
736 if(point.y < bounds.center.y) { 0 } { plots.size - 1 }
740 getDataPoint { |x, y|
741 var plotIndex = this.pointIsInWhichPlot(x @ y);
743 plots.at(plotIndex).getDataPoint(x)
747 postCurrentValue { |x, y|
748 this.getDataPoint(x, y).postln
752 var plotIndex = this.pointIsInWhichPlot(x @ y);
754 plots.at(plotIndex).editData(x, y, plotIndex);
759 parent !? { parent.refresh }
762 // private implementation
765 var size, array = item.asArray;
766 if(item.first.isSequenceableCollection.not) {
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 };
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)) {
800 plotter.setValue(array, true, false);
802 minval !? { plotter.minval = minval; };
803 maxval !? { plotter.maxval = maxval };
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;
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;
832 this.loadToFloatArray(duration, server, { |array, buf|
833 var numChan = buf.numChannels;
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 };
851 plot2 { |name, bounds, minval, maxval|
852 ^this.asSignal.plot2(name, bounds, minval: minval, maxval: maxval)
858 plot2 { |name, bounds, minval, maxval|
860 if(server.serverRunning.not) { "Server % not running".format(server).warn; ^nil };
862 name ? "Buffer plot (bufnum: %)".format(this.bufnum),
863 bounds, minval: minval, maxval: maxval
865 this.loadToFloatArray(action: { |array, buf|
867 plotter.value = array.unlace(buf.numChannels);
868 plotter.setProperties(\labelX, "frames");
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");