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;
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),
21 gridLineSmoothing: false,
25 gridFont: Font( Font.defaultSansFace, 9 )
31 ^super.newCopyArgs(plotter).init
36 var gui = plotter.gui;
37 var skin = GUI.skin.at(\plot);
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);
44 font = ~gridFont ?? { gui.font.default };
45 if(font.class != gui.font) {
47 if( gui.font.availableFonts.detect(_ == fontName).isNil, { fontName = gui.font.defaultSansFace });
48 font = gui.font.new(fontName, font.size)
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)};
63 var size = (try { "foo".bounds(font).height } ?? { font.size } * 1.5);
64 plotBounds = if(rect.height > 40) { rect.insetBy(size, size) } { rect };
67 drawGrid.bounds = plotBounds;
76 if(gridOnY and: spec.notNil,{
77 drawGrid.vertGrid = spec.grid;
79 drawGrid.vertGrid = nil
84 if(gridOnX and: domainSpec.notNil,{
85 drawGrid.horzGrid = domainSpec.grid;
87 drawGrid.horzGrid = nil
91 drawGrid.x.gridColor = c;
95 drawGrid.y.gridColor = c;
104 drawGrid.fontColor = c;
106 gridLineSmoothing_ { |bool|
107 drawGrid.smoothing = bool;
109 gridLinePattern_ { |pattern|
110 drawGrid.linePattern = pattern;
114 drawGrid.horzGrid = if(gridOnX,{domainSpec.grid},{nil});
118 drawGrid.vertGrid = if(gridOnY,{spec.grid},{nil});
126 plotter.drawFunc.value(this); // additional elements
131 pen.fillColor = backgroundColor;
137 if(gridOnX and: { labelX.notNil }) {
138 sbounds = try { labelX.bounds(font) } ? 0;
140 pen.strokeColor = fontColor;
141 pen.stringAtPoint(labelX,
142 plotBounds.right - sbounds.width @ plotBounds.bottom
145 if(gridOnY and: { labelY.notNil }) {
146 sbounds = try { labelY.bounds(font) } ? 0;
148 pen.strokeColor = fontColor;
149 pen.stringAtPoint(labelY,
150 plotBounds.left - sbounds.width - 3 @ plotBounds.top
155 domainCoordinates { |size|
156 var val = this.resampledDomainSpec.unmap(plotter.domain ?? { (0..size-1) });
157 ^plotBounds.left + (val * plotBounds.width);
161 var val = spec.unmap(this.prResampValues);
162 ^plotBounds.bottom - (val * plotBounds.height); // measures from top left (may be arrays)
166 ^min(value.size, plotBounds.width / plotter.resolution)
169 resampledDomainSpec {
170 var offset = if(this.hasSteplikeDisplay) { 0 } { 1 };
171 ^domainSpec.copy.maxval_(this.resampledSize - offset)
175 var mode = plotter.plotMode;
176 var ycoord = this.dataCoordinates;
177 var xcoord = this.domainCoordinates(ycoord.size);
181 plotColor = plotColor.as(Array);
183 if(ycoord.at(0).isSequenceableCollection) { // multi channel expansion
184 ycoord.flop.do { |y, i|
186 this.perform(mode, xcoord, y);
187 pen.strokeColor = plotColor.wrapAt(i);
192 pen.strokeColor = plotColor.at(0);
193 this.perform(mode, xcoord, ycoord);
203 pen.moveTo(x.first @ y.first);
205 pen.lineTo(x[i] @ y[i]);
210 var size = min(bounds.width / value.size * 0.25, 4);
212 pen.addArc(x[i] @ y[i], 0.5, 0, 2pi);
213 if(size > 2) { pen.addArc(x[i] @ y[i], size, 0, 2pi); };
218 var size = min(bounds.width / value.size * 0.25, 3);
219 pen.moveTo(x.first @ y.first);
223 pen.addArc(p, size, 0, 2pi);
229 pen.smoothing_(false);
231 pen.moveTo(x[i] @ y[i]);
232 pen.lineTo(x[i + 1] ?? { plotBounds.right } @ y[i]);
237 pen.smoothing_(false);
238 pen.moveTo(x.first @ y.first);
240 pen.lineTo(x[i] @ y[i]);
241 pen.lineTo(x[i + 1] ?? { plotBounds.right } @ y[i]);
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);
256 editData { |x, y, plotIndex|
257 var index = this.getIndex(x);
258 this.editDataIndex( index, x, y, plotIndex );
261 editDataLine { |pt1, pt2, plotIndex|
262 var ptLeft, ptRight, ptLo, ptHi;
264 var i1, i2, iLo, iHi;
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
273 ^this.editDataIndex( i2, pt2.x, pt2.y, plotIndex );
276 // order points and indexes
279 ptLeft = pt1; ptRight = pt2;
282 ptLeft = pt2; ptRight = pt1;
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 );
292 // trigger once for second end of the line
293 plotter.editFunc.value(plotter, plotIndex, i2, val, pt2.x, pt2.y);
298 // get actual points corresponding to indexes
299 xSpec = ControlSpec( ptLeft.x, ptRight.x );
300 ySpec = ControlSpec( ptLeft.y, ptRight.y );
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 );
317 // trigger once for second end of the line
318 plotter.editFunc.value(plotter, plotIndex, i2, val, pt2.x, pt2.y);
322 getRelativePositionX { |x|
323 ^domainSpec.map((x - plotBounds.left) / plotBounds.width)
326 getRelativePositionY { |y|
327 ^spec.map((plotBounds.bottom - y) / plotBounds.height)
331 ^#[\levels, \steps].includes(plotter.plotMode)
335 var offset = if(this.hasSteplikeDisplay) { 0.5 } { 0.0 }; // needs to be fixed.
336 ^(this.getRelativePositionX(x) - offset).round.asInteger
340 var index = this.getIndex(x).clip(0, value.size - 1);
341 ^[index, value.at(index)]
348 font.size = max(1, font.size + val);
353 ^super.copy.drawGrid_(drawGrid.copy)
356 ^if(value.size <= (plotBounds.width / plotter.resolution)) {
359 valueCache ?? { valueCache = value.resamp1(plotBounds.width / plotter.resolution) }
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;
379 *new { |name, bounds, parent|
380 ^super.newCopyArgs(name).makeWindow(parent, bounds)
383 makeWindow { |argParent, argBounds|
384 parent = argParent ? parent;
385 bounds = argBounds ? bounds;
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 };
394 parent.onClose = { parent = nil };
397 bounds = bounds ?? { parent.bounds.moveTo(0, 0) };
398 interactionView = gui.userView.new(parent, bounds);
399 interactionView.drawFunc = { this.draw };
401 modes = [\points, \levels, \linear, \plines, \steps].iter.loop;
404 .background_(Color.clear)
405 .focusColor_(Color.clear)
408 .mouseDownAction_({ |v, x, y, modifiers|
411 editPlotIndex = this.pointIsInWhichPlot(cursorPos);
413 editPos = x @ y; // new Point instead of cursorPos!
415 plots.at(editPlotIndex).editData(x, y, editPlotIndex);
416 if(this.numFrames < 200) { this.refresh };
420 if(modifiers.isAlt) { this.postCurrentValue(x, y) };
422 .mouseMoveAction_({ |v, x, y, modifiers|
424 if(superpose.not && editPlotIndex.notNil) {
426 plots.at(editPlotIndex).editDataLine(editPos, cursorPos, editPlotIndex);
427 if(this.numFrames < 200) { this.refresh };
429 editPos = x @ y; // new Point instead of cursorPos!
432 if(modifiers.isAlt) { this.postCurrentValue(x, y) };
437 if(editMode && superpose.not) { this.refresh };
439 .keyDownAction_({ |view, char, modifiers, unicode, keycode|
440 if(modifiers.isCmd.not) {
442 // y zoom out / font zoom
444 if(modifiers.isCtrl) {
445 plots.do(_.zoomFont(-2));
447 this.specs = specs.collect(_.zoom(3/2));
451 // y zoom in / font zoom
453 if(modifiers.isCtrl) {
454 plots.do(_.zoomFont(2));
456 this.specs = specs.collect(_.zoom(2/3));
462 this.calcSpecs(separately: false);
463 this.updatePlotSpecs;
467 /*// x zoom out (doesn't work yet)
469 this.domainSpecs = domainSpecs.collect(_.zoom(3/2));
471 // x zoom in (doesn't work yet)
473 this.domainSpecs = domainSpecs.collect(_.zoom(2/3))
480 this.specs = specs.collect(_.normalize)
483 this.updatePlotSpecs;
485 normalized = normalized.not;
490 plots.do { |x| x.gridOnY = x.gridOnY.not }
492 // toggle domain grid
494 plots.do { |x| x.gridOnX = x.gridOnX.not };
498 this.plotMode = modes.next;
502 editMode = editMode.not;
503 "plot edit mode %\n".postf(if(editMode) { "on" } { "off" });
505 // toggle superposition
507 this.superpose = this.superpose.not;
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))
523 .focusColor_(Color.clear)
526 .action_ { this.class.openHelpFile };
531 this.setValue(arrays, findSpecs, true)
534 setValue { |arrays, findSpecs = true, refresh = true|
536 data = this.prReshape(arrays);
539 this.calcDomainSpecs;
541 this.updatePlotSpecs;
543 if(refresh) { this.refresh };
549 this.setValue(value, false, true);
558 if(value.isNil) { ^0 };
567 this.updatePlotBounds;
570 plots.do { |plot| plot.draw };
575 ^interactionView.bounds.insetBy(9, 8)
581 var deltaY = if(data.size > 1 ) { 4.0 } { 0.0 };
582 var distY = bounds.height / data.size;
583 var height = distY - deltaY;
587 Rect(bounds.left, distY * i + bounds.top, bounds.width, height)
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;
603 if(plots.size != data.size) {
607 plot.value = data.at(i)
615 plot.spec = specs.clipAt(i)
620 plot.domainSpec = domainSpecs.clipAt(i)
625 setProperties { |... pairs|
626 pairs.pairsDo { |selector, value|
627 selector = selector.asSetter;
628 plots.do { |x| x.perform(selector, value) }
635 specs = argSpecs.asArray.clipExtend(data.size).collect(_.asSpec);
636 this.updatePlotSpecs;
639 domainSpecs_ { |argSpecs|
640 domainSpecs = argSpecs.asArray.clipExtend(data.size).collect(_.asSpec);
641 this.updatePlotSpecs;
646 specs.do { |x, i| x.minval = val.wrapAt(i) };
647 this.updatePlotSpecs;
652 specs.do { |x, i| x.maxval = val.wrapAt(i) };
653 this.updatePlotSpecs;
657 calcSpecs { |separately = true|
658 specs = (specs ? [\unipolar.asSpec]).clipExtend(data.size);
660 this.specs = specs.collect { |spec, i|
661 var list = data.at(i);
662 list !? { spec = spec.looseRange(list.flat) };
665 this.specs = specs.first.looseRange(data.flat);
670 // for now, a simple version
671 domainSpecs = data.collect { |val|
672 [0, val.size - 1, \lin, 1].asSpec
678 pointIsInWhichPlot { |point|
679 var res = plots.detectIndex { |plot|
680 point.y.exclusivelyBetween(plot.bounds.top, plot.bounds.bottom)
683 if(point.y < bounds.center.y) { 0 } { plots.size - 1 }
687 getDataPoint { |x, y|
688 var plotIndex = this.pointIsInWhichPlot(x @ y);
690 plots.at(plotIndex).getDataPoint(x)
694 postCurrentValue { |x, y|
695 this.getDataPoint(x, y).postln
699 var plotIndex = this.pointIsInWhichPlot(x @ y);
701 plots.at(plotIndex).editData(x, y, plotIndex);
706 parent !? { parent.refresh }
710 var size, array = item.asArray;
711 if(item.first.isSequenceableCollection.not) {
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 };
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)) {
737 plotter.setValue(array, true, false);
738 if(minval.notNil and: {maxval.notNil},{
739 plotter.specs = [minval,maxval].asSpec
741 minval !? { plotter.minval = minval; };
742 maxval !? { plotter.maxval = maxval };
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;
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) {
772 Error("loadToFloatArray failed: % is no valid UGen input".format(val)).throw
774 val = UGen.replaceZeroesWithSilence(val.asArray);
776 if(rate == \audio) { // convert mixed rate outputs:
777 val = val.collect { |x| if(x.rate != \audio) { K2A.ar(x) } { x } }
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);
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);
797 server.sendMsg("/d_free", name);
799 }).add.removeWhenDone;
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;
809 this.loadToFloatArray(duration, server, { |array, buf|
810 var numChan = buf.numChannels;
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);
825 plot { |name, bounds, minval, maxval|
826 ^this.asSignal.plot(name, bounds, minval: minval, maxval: maxval)
831 plot { |name, bounds, minval, maxval|
833 if(server.serverRunning.not) { "Server % not running".format(server).warn; ^nil };
835 name ? "Buffer plot (bufnum: %)".format(this.bufnum),
836 bounds, minval: minval, maxval: maxval
838 this.loadToFloatArray(action: { |array, buf|
840 if(minval.notNil and: {maxval.notNil},{
841 plotter.specs = [minval,maxval].asSpec
843 minval !? { plotter.minval = minval; };
844 maxval !? { plotter.maxval = maxval };
846 plotter.domainSpecs = ControlSpec(0.0,buf.numFrames,units:"frames");
847 plotter.setValue(array.unlace(buf.numChannels),false,true);
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");
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)