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 ~ 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 +/