1 /**
2     Copyright: Copyright (c) 2018- Alexander Orlov. All rights reserved.
3     License: $(LINK2 https://opensource.org/licenses/MIT, MIT License).
4     Authors: Alexander Orlov, $(LINK2 mailto:sascha.orlov@gmail.com, sascha.orlov@gmail.com) 
5 */
6 
7 /**
8     A simplistic binding for gnuplot manipulation via pipes. 
9     See http://ndevilla.free.fr/gnuplot/ for original, however not official C-bindings. 
10     Based on 2.11 version. However, ignoring all windows features. 
11 */
12 
13 module gnuplotd; 
14 
15 import std.process : pipeProcess, Redirect; 
16 import std.stdio : File;
17 import std.algorithm.iteration : each, filter; 
18 import core.stdc.limits : CHAR_BIT; 
19 import std.range; 
20 import std.path; 
21 debug
22 {
23     import std.algorithm; 
24 }
25 
26 /**
27 @brief    gnuplot session handle (opaque type).
28 
29 This structure holds all necessary information to talk to a gnuplot
30 session. It is built and returned by start() and later used
31 by all functions in this module to communicate with the session, then
32 meant to be closed by finish().
33 
34 This structure is meant to remain opaque, you normally do not need
35 to know what is contained in there.
36 */
37 enum GP_MAX_TMP_FILES = size_t.sizeof * CHAR_BIT; 
38 private struct GnuPlotCtrl
39 {	
40     /** Pipe to gnuplot process */
41     typeof(pipeProcess("gnuplot", Redirect.stdin)) gnucmd; 
42 
43     /** Number of currently active plots */
44     size_t nplots;
45 
46     /** Current plotting style */
47     string pstyle = "points";
48 
49     /** Pointer to table of names of temporary files */
50     string[GP_MAX_TMP_FILES] tmp_filename_tbl;
51     
52     /** Number of temporary files */
53     size_t ntmp;
54 }
55 
56 /**
57 @brief    Opens up a gnuplot session, ready to receive commands.
58 @return   Newly created gnuplot control structure.
59 
60 This opens up a new gnuplot session, ready for input. The struct
61 controlling a gnuplot session should remain opaque and only be
62 accessed through the provided functions.
63 
64 The session must be closed using finish().
65 */
66 auto start()
67 {
68 	import std.process : ProcessException; 
69 
70 	GnuPlotCtrl gpc; 
71 
72 	try 
73 	{
74 		gpc.gnucmd = pipeProcess("gnuplot", Redirect.stdin); 
75 	}
76 	catch(ProcessException pe)
77 	{
78 		import core.stdc.stdlib : exit;
79 		import std.stdio : stderr;
80 		
81 		stderr.writeln("There was an error while connecting to gnuplot. Please check proper installation"); 
82 		stderr.writeln(pe.msg); 
83 
84 		exit(1);
85 	}
86 	return gpc; 
87 }
88 
89 /**
90 finalizer for gnuplot control object. returns the underlying stream 
91 */
92 auto finish(ref GnuPlotCtrl gpc)
93 {
94     import std.file : remove;    
95     gpc.tmp_filename_tbl[].filter!(el => !el.empty).each!(s => s.remove); 
96 	return gpc.gnucmd; 
97 }
98 
99 /**
100 general put function for the gnuplot controller structure
101 */
102 void put(Args...)(ref GnuPlotCtrl gpc, string cmd, Args args)
103 {
104 	gpc.gnucmd.stdin.writefln(cmd, args); 
105 	gpc.gnucmd.stdin.flush; 
106 }
107 
108 /**
109 style setting for the gnuplot controller structure.
110 */
111 void setStyle(ref GnuPlotCtrl gpc, string plot_style)
112 {
113 	import core.stdc.string : strcmp, strcpy; 
114 	static immutable string[] knownStyles = 
115 	["lines", "points", "linespoints", "impulses", "dots", "steps", "errorbars", "boxes", "boxerrorbars"]; 
116 	import std.algorithm : canFind; 
117 	import core.stdc.stdio : fprintf;
118 	import std.stdio : stderr; 
119 	if(!knownStyles.canFind(plot_style))
120     {
121         stderr.writeln("warning: unknown requested style: using points") ;
122         gpc.pstyle = "points";
123     } else {
124     	gpc.pstyle = plot_style; 
125     }
126 }
127 
128 /**
129 x label setting for gnuplot controller structure
130 */
131 void setXlabel(ref GnuPlotCtrl gpc, string label)
132 {
133     put(gpc, "set xlabel \"%s\"", label) ;
134 }
135 
136 /**
137 y label setting for gnuplot controller structure
138 */
139 void setYlabel(ref GnuPlotCtrl gpc, string label)
140 {
141     put(gpc, "set ylabel \"%s\"", label) ;
142 }
143 
144 /**
145 recreation (reusing) of gnuplot controller structure
146 */
147 void resetPlot(ref GnuPlotCtrl gpc)
148 {
149     gpc.finish; 
150     gpc = start; 
151 }
152 
153 /**
154 x plotting for gnuplot controller structure
155 */
156 void plotX(Range)(ref GnuPlotCtrl gpc, Range darr, string title)
157 {
158     if (!gpc.gnucmd.stdin.isOpen || darr.empty) return;
159 
160     /* Open temporary file for output   */
161     string tmpfname = tmpFile(gpc);
162     import std.stdio : File, stderr; 
163     auto tmpfd = File(tmpfname, "w");
164     
165     if (!tmpfd.isOpen) {
166         stderr.writeln("cannot create temporary file: exiting plot") ;
167         return;
168     }
169 
170     /* Write data to this file  */
171     darr.each!(d => tmpfd.writefln("%.18e", d));
172 
173     //fclose(tmpfd) ;
174 
175     plotAtmpfile(gpc, tmpfname, title);
176 }
177 
178 /**
179 xy plotting for gnuplot controller structure
180 */
181 void plotXY(RangeX, RangeY)(ref GnuPlotCtrl gpc, RangeX xarr, RangeY yarr, string title)
182 {
183     if (!gpc.gnucmd.stdin.isOpen || xarr.empty || yarr.empty) return;
184     
185     import std.stdio : File, stderr; 
186     if(xarr.length != yarr.length)
187     {
188     	
189     	stderr.writeln("x and y have different lengths");
190     	return; 
191     }
192 
193     /* Open temporary file for output   */
194     string tmpfname = tmpFile(gpc);
195     auto tmpfd = File(tmpfname, "w");
196 
197     if (!tmpfd.isOpen) {
198         stderr.writeln("cannot create temporary file: exiting plot");
199         return;
200     }
201 
202     /* Write data to this file  */
203 
204     import std.range : iota; 
205     iota(xarr.length).each!(i => tmpfd.writefln("%.18e %.18e", xarr[i], yarr[i]));
206 
207     //fclose(tmpfd) ;
208 
209     plotAtmpfile(gpc, tmpfname, title);
210     return ;
211 }
212 
213 /**
214 a common interface for a single plotting event for gnuplot controller structure
215 */
216 void plotOnce(RangeX, RangeY)(string title, string style, string label_x, string label_y, RangeX x, RangeY y)
217 {
218 	auto gpc = start;
219 
220 	if (x.empty) return;
221 
222 	if (!gpc.gnucmd.stdin.isOpen) return ;
223 
224 	if (!style.empty)
225 	{
226 	    setStyle(gpc, style);
227 	}
228 	else
229 	{
230 	    setStyle(gpc, "lines");
231 	}
232 
233 	if (!label_x.empty)
234 	{
235 	    setXlabel(gpc, label_x);
236 	}
237 	else
238 	{
239 	    setXlabel(gpc, "X");
240 	}
241 
242 	if (!label_y.empty)
243 	{
244 	    setYlabel(gpc, label_y);
245 	}
246 	else
247 	{
248 	    setYlabel(gpc, "Y");
249 	}
250 
251 	if (y.empty)
252 	{
253 	    plotX(gpc, x, title);
254 	}
255 	else
256 	{
257 	    plotXY(gpc, x, y, title);
258 	}
259 	import std.stdio : writeln;
260 	writeln("press ENTER to continue");
261 
262     import core.stdc.stdio : getchar; 
263 	while (getchar()!='\n') {}
264 	  
265 	gpc.finish; 
266 }
267 
268 /**
269 slope plotting for gnuplot controller structure
270 */
271 void plotSlope(ref GnuPlotCtrl gpc, double a, double b, string title)
272 {
273     string cmd = (gpc.nplots > 0) ? "replot" : "plot";
274     
275     title = title.empty ? "(none)" : title;
276 
277     put(gpc, "%s %.18e * x + %.18e title \"%s\" with %s", cmd, a, b, title, gpc.pstyle);
278 
279     gpc.nplots++;
280 }
281 
282 /**
283 equation plotting for gnuplot controller structure
284 */
285 void plotEquation(ref GnuPlotCtrl gpc, string equation, string title)
286 {
287  	string cmd = gpc.nplots ? "replot" : "plot";
288     title = title.empty ? "(none)" : title;
289 
290     put(gpc, "%s %s title \"%s\" with %s", cmd, equation, title, gpc.pstyle);
291     gpc.nplots++;
292 }
293 
294 private string tmpFile(ref GnuPlotCtrl gpc)
295 {
296 	import std.uuid : randomUUID; 
297     import std.file : tempDir; 
298 
299     assert(gpc.tmp_filename_tbl[gpc.ntmp] is null);
300     import std.stdio : File, stderr; 
301     /* Open one more temporary file? */
302     if (gpc.ntmp == GP_MAX_TMP_FILES - 1) {
303         stderr.writefln("maximum # of temporary files reached (%d): cannot open more", GP_MAX_TMP_FILES);
304         return string.init;
305     }
306 
307     gpc.tmp_filename_tbl[gpc.ntmp] = tempDir ~ dirSeparator ~ randomUUID.toString;
308     gpc.ntmp++;
309     return gpc.tmp_filename_tbl[gpc.ntmp - 1];
310 }
311 
312 
313 private void plotAtmpfile(ref GnuPlotCtrl gpc, string tmp_filename, string title)
314 {
315     string cmd = (gpc.nplots > 0) ? "replot" : "plot";
316     title = (title.empty) ? "(none)" : title;
317     put(gpc, "%s \"%s\" title \"%s\" with %s", cmd, tmp_filename, title, gpc.pstyle);
318     gpc.nplots++;
319 }
320 
321 /**
322 csv dumping for gnuplot controller structure (x var only)
323 */
324 int writeXcsv(Range)(string fileName, Range darr, string title)
325 {
326     if (fileName.empty || darr.empty)
327     {
328         return -1;
329     }
330 
331     auto fileHandle = File(fileName, "w");
332 
333     if (!fileHandle.isOpen)
334     {
335         return -1;
336     }
337 
338     // Write Comment.
339     if (!title.empty)
340     {
341         fileHandle.writefln("# %s", title);
342     }
343 
344     /* Write data to this file  */
345     darr.each!((i, d) => fileHandle.writefln("%d, %.18e", i, d));
346 
347     return 0;
348 }
349 
350 /**
351 csv dumping for gnuplot controller structure (xy vars)
352 */
353 int writeXYcsv(RangeX, RangeY)(string fileName, RangeX xarr, RangeY yarr, string title)
354 {
355     if (fileName.empty || xarr.empty || yarr.empty)
356     {
357         return -1;
358     }
359 
360     auto fileHandle = File(fileName, "w");
361 
362     if (!fileHandle.isOpen)
363     {
364         return -1;
365     }
366 
367     // Write Comment.
368     if (!title.empty)
369     {
370         fileHandle.writefln("# %s", title);
371     }
372 
373     /* Write data to this file  */
374     iota(xarr.length).each!(i => fileHandle.writefln("%.18e, %.18e", xarr[i], yarr[i]));
375 
376     return 0;
377 }
378 
379 /**
380 csv dumping for gnuplot controller structure (many columns)
381 */
382 int writeMultiCsv(RoR)(string fileName, RoR xListPtr, string title)
383 {
384     if (fileName.empty || xListPtr.empty || xListPtr.front.empty)
385     {
386         return -1;
387     }
388 
389     auto fileHandle = File(fileName, "w");
390 
391     if (!fileHandle.isOpen)
392     {
393         return -1;
394     }
395 
396     // Write Comment.
397     if (!title.empty)
398     {
399         fileHandle.writefln("# %s", title);
400     }
401 
402     /* Write data to this file  */
403     
404     for (auto j = 0; j < xListPtr.front.length; j++)
405     {
406         fileHandle.writef("%d, %.18e", j, xListPtr[0][j]);
407 
408     	for (auto i = 1; i < xListPtr.length; i++)    
409         {
410             fileHandle.writef(", %.18e", xListPtr[i][j]);
411         }
412 
413         fileHandle.writeln;
414     }
415 
416     return 0;
417 }
418 
419 ///
420 unittest
421 {
422     auto gpc = start; 
423     assert(gpc.gnucmd.stdin.isOpen); 
424     gpc.finish; 
425 }
426 
427 ///
428 unittest
429 {
430     import std.stdio : writeln; 
431 	auto gpc = gnuplotd.start; 
432 	assert(gpc.pstyle == "points");
433 	assert(gpc.gnucmd.stdin.isOpen); 
434     gpc.put("set terminal gif animate"); // delay 100 (= 1 sec)
435     gpc.put("set terminal gif animate");
436     gpc.put("set output \"./tests/anim1.gif\"");
437 	double phase;
438 	
439     writeln("*** example of gnuplot control through D ***");
440     /*
441 
442     set terminal gif animate delay 100
443     set output 'foobar.gif'
444     stats 'datafile' nooutput
445     set xrange [-0.5:1.5]
446     set yrange [-0.5:5.5]
447 
448     do for [i=1:int(STATS_blocks)] {
449         plot 'datafile' index (i-1) with circles
450     }
451     
452     */
453     for (phase = 0.1; phase < 10; phase += 0.1)
454     {
455         gpc.put("plot sin(x+%g)", phase);
456     }
457     
458     for (phase = 10; phase >= 0.1; phase -= 0.1)
459     {
460         gpc.put("plot sin(x+%g)", phase);
461     }
462     gpc.finish; 
463 }
464 
465 ///
466 unittest
467 {
468     import std.stdio : writeln; 
469 	import core.thread : Thread; 
470 	import core.time : dur; 
471 	enum SLEEP_LGTH = dur!("seconds")(2); 
472 	enum NPOINTS = 50; 
473 	auto gpc1 = start; 
474 	auto gpc2 = start; 
475 	auto gpc3 = start; 
476 	auto gpc4 = start; 
477 
478 	double[NPOINTS] x;
479     double[NPOINTS] y;
480     int i;
481 
482     /*
483      * Initialize the gnuplot handle
484      */
485     writeln("*** example of gnuplot control through D ***");
486 
487     /*
488      * Slopes
489      */
490     gpc1.setStyle("lines") ;
491     
492     writeln("*** plotting slopes");
493     writeln("y = x");
494     gpc1.plotSlope(1.0, 0.0, "unity slope") ;
495     Thread.sleep(SLEEP_LGTH) ;
496 
497     writeln("y = 2*x") ;
498     gpc1.plotSlope(2.0, 0.0, "y=2x") ;
499     Thread.sleep(SLEEP_LGTH) ;
500 
501     writeln("y = -x") ;
502     gpc1.plotSlope(-1.0, 0.0, "y=-x") ;
503     Thread.sleep(SLEEP_LGTH) ;
504 
505     
506     /*
507      * Equations
508      */
509     gpc1.resetPlot;
510     writeln();
511     writeln();
512     writeln("*** various equations");
513     writeln("y = sin(x)");
514     gpc1.plotEquation("sin(x)", "sine");
515     Thread.sleep(SLEEP_LGTH);
516 
517     writeln("y = log(x)");
518     gpc1.plotEquation("log(x)", "logarithm");
519     Thread.sleep(SLEEP_LGTH) ;
520 
521     writeln("y = sin(x)*cos(2*x)");
522     gpc1.plotEquation("sin(x)*cos(2*x)", "sine product");
523     Thread.sleep(SLEEP_LGTH) ;
524 
525 
526     /*
527      * Styles
528      */
529     gpc1.resetPlot;
530     writeln(); 
531     writeln();
532 
533     writeln("*** showing styles");
534 
535     writeln("sine in points");
536     gpc1.setStyle("points");
537     gpc1.plotEquation("sin(x)", "sine");
538     Thread.sleep(SLEEP_LGTH) ;
539     
540     writeln("sine in impulses") ;
541     gpc1.setStyle("impulses");
542     gpc1.plotEquation("sin(x)", "sine");
543     Thread.sleep(SLEEP_LGTH) ;
544     
545     writeln("sine in steps");
546     gpc1.setStyle("steps");
547     gpc1.plotEquation("sin(x)", "sine");
548     Thread.sleep(SLEEP_LGTH) ;
549 
550     /*
551      * User defined 1d and 2d point sets
552      */
553     gpc1.resetPlot;
554     gpc1.setStyle("impulses");
555     writeln(); 
556     writeln(); 
557 
558     writeln("*** user-defined lists of doubles");
559     for (i=0 ; i<NPOINTS ; i++) {
560         x[i] = cast(double)i*i ;
561     }
562     gpc1.plotX(x, "user-defined doubles");
563     Thread.sleep(SLEEP_LGTH) ;
564 
565 	writeln("*** user-defined lists of points");
566     for (i=0 ; i<NPOINTS ; i++) {
567         x[i] = cast(double)i ;
568         y[i] = cast(double)i * cast(double)i ;
569     }
570     gpc1.resetPlot;
571     gpc1.setStyle("points");
572     gpc1.plotXY(x, y, "user-defined points");
573     Thread.sleep(SLEEP_LGTH);
574 
575 
576     /*
577      * Multiple output screens
578      */
579 
580     writeln();
581     writeln();
582 
583     writeln("*** multiple output windows");
584     gpc1.resetPlot;
585     gpc1.setStyle("lines");
586     gpc2.setStyle("lines");
587     gpc3.setStyle("lines");
588     gpc4.setStyle("lines");
589 
590     writeln("window 1: sin(x)");
591     gpc1.plotEquation("sin(x)", "sin(x)");
592     Thread.sleep(SLEEP_LGTH) ;
593     writeln("window 2: x*sin(x)");
594     gpc2.plotEquation("x*sin(x)", "x*sin(x)");
595     Thread.sleep(SLEEP_LGTH) ;
596     writeln("window 3: log(x)/x");
597     gpc3.plotEquation("log(x)/x", "log(x)/x");
598     Thread.sleep(SLEEP_LGTH) ;
599     writeln("window 4: sin(x)/x");
600     gpc4.plotEquation("sin(x)/x", "sin(x)/x");
601     Thread.sleep(SLEEP_LGTH) ;
602     
603     gpc1.finish; 
604     gpc2.finish; 
605     gpc3.finish; 
606     gpc4.finish; 
607     writeln(); 
608     writeln();
609     writeln("*** end of gnuplot example");
610 }
611 /+
612 ///
613 unittest
614 {
615 	auto gpc = gnuplotd.start; 
616 	assert(gpc.pstyle == "points");
617 	assert(gpc.gnucmd.stdin.isOpen); 
618 	gpc.put("set terminal png");
619 	gpc.put("set output \"./tests/sine.png\"");
620 	gpc.plotEquation("sin(x)", "Sine wave");
621     gpc.finish; 
622 }
623 
624 ///
625 unittest
626 {
627 	writeXcsv("./tests/testfile_x.csv", [1.1, 1.2, 1.3], "test1, test2");
628 	writeXYcsv("./tests/testfile_xy.csv", [1.1, 1.2, 1.3], [2.1, 2.2, 2.3], "test1, test2");
629 	
630 	double[][] multi; 
631 	multi.length = 3; 
632 	import std.algorithm : each; 
633 	multi.each!((ref c) => c.length = 5); 
634 
635 	multi[0][0] = 0.0;
636 	multi[1][0] = 1.0; 
637 	multi[2][0] = 2.0; 
638 	multi[0][1] = 3.0; 
639 	multi[1][1] = 4.0; 
640 	multi[2][1] = 5.0; 
641 	multi[0][2] = 6.0; 
642 	multi[1][2] = 7.0; 
643 	multi[2][2] = 8.0; 
644 	multi[0][3] = 9.0; 
645 	multi[1][3] = 10.0; 
646 	multi[2][3] = 11.0; 
647 	multi[0][4] = 12.0; 
648 	multi[1][4] = 13.0; 
649 	multi[2][4] = 14.0; 
650 	//writeln(multi);
651 	writeMultiCsv("./tests/testfile_multi.csv", multi, "test1, test2");
652 }
653 
654 ///
655 unittest
656 {
657     import std.stdio : writeln; 
658     auto gpc = gnuplotd.start;
659     assert(gpc.pstyle == "points");
660     assert(gpc.gnucmd.stdin.isOpen);
661     gnuplotd.put(gpc, "set terminal png");
662     gnuplotd.put(gpc, "set output \"./tests/test.png\"");
663 
664     enum NPOINTS = 50; 
665     double[NPOINTS] x;
666     double[NPOINTS] y;
667 
668     writeln("*** user-defined lists of points");
669     for (auto i=0 ; i<NPOINTS ; i++) {
670         x[i] = cast(double)i ;
671         y[i] = cast(double)i * cast(double)i ;
672     }
673 
674     
675     gpc.plotXY(x, y, "user-defined points");
676     const(double)[] xConst = x[]; 
677     const(double)[] yConst = y[]; 
678     gpc.plotXY(xConst, yConst, "user-defined points");
679     gpc.finish; 
680 }
681 +/