File: gnuplot_common_.ml

package info (click to toggle)
ocaml-gnuplot 0.8.3-3
  • links: PTS, VCS
  • area: main
  • in suites: buster, jessie, jessie-kfreebsd, stretch, wheezy
  • size: 368 kB
  • ctags: 522
  • sloc: ml: 2,147; makefile: 185
file content (1481 lines) | stat: -rw-r--r-- 55,390 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
(* Copyright (C) 2001-2009

     Christophe Troestler
     email: Christophe.Troestler@umh.ac.be
     WWW: http://math.umh.ac.be/an/software/

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public License
   version 2.1 as published by the Free Software Foundation, with the
   special exception on linking described in file LICENSE.

   This library is distributed in the hope that it will be useful, but
   WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the file
   LICENSE for more details.
*)


(**********************************************************************
 *
 * Data structures & basic functions
 *
 **********************************************************************)

open Printf

type color = int

exception Exit of Unix.process_status

let is_finite x =
  match classify_float x with
  | FP_infinite | FP_nan -> false
  | _ -> true

(* [force_show gp] force the drawing of axes, tics,... by plotting an
   "empty" file.  That will only work IF the RANGES have been SET.
   Beware that, unless multiplot mode is on, this will also erase all
   the plots.  (Note that the missing symbol must be declared as "?".)
*)
let force_show gp =
  output_string gp "plot '-' title \"\"\n? ?\ne;\n"

(*
 * Plot history
 **********************************************************************)

(* Plots will be grouped by tags.  When no tag is provided, the system
   one will be assigned -- which implies that by default the plots
   will be managed automatically by the system. *)
type tag = int

module Tags = Map.Make(struct
                         type t = tag
                         let compare (x:tag) y = compare x y
                       end)

type tag_state =
  | Managed  (* Visible and the system determines if replot is needed *)
  | Visible  (* Set visible by user -- only reploted if the user ask for it *)
  | Hidden   (* Erased, not on display anymore *)
  | To_show  (* scheduled to make Visible -- by [show] or a plot command *)
  | To_hide  (* scheduled to erase *)

(* Experiments show that passing the data through the pipe to gnuplot
   is not efficient unless there are very few lines of data. *)
type data =
  | Inlined of string (* data in the form "line1\nline2\n...\ne\n" *)
  | File of string    (* filename containing the data *)

type plot = string * float * int * color * data
    (* [(cmd, lw, lt, color, data)] where [cmd] is something like
       ["\"datafile\" axes x1y2 title \"\" with pt 1 ps 2"], [lw] is
       the line width, and [lt] the value of the linetype of the plot.
       The gnuplot command to show such a plot is therefore [sprintf
       "plot %s lw %F lt %i" cmd lw lt].  If the [data] is inlined,
       one must follow the command "plot..." with the data in the same
       order than the '-'. *)

type text = string * int
    (* [(cmd, gp_tag)] where [cmd] is something like ["set label
       \"...\" ... tc lt 2"] and [gp_tag] is the gnuplot tag (NOT the
       tag of this library) and we will subsequently refer to it as
       the label number.  *)

type plots = {
  mutable state : tag_state;
  plots : plot Queue.t;
  texts : text Queue.t;
}

(* [string_of_queue init q] will add to [init] (assumed to end with
   ",") the instructions to plot all data in the queue [q].  *)
let string_of_queue init q =
  Queue.fold (fun cmd (s, lw, lt, color, _) ->
                sprintf "%s%s lw %F lt %i %s," cmd s lw lt
                  (if color >= 0 then sprintf "lc rgb \"#%X\"" color else "")
             ) init q.plots

(* NOTE: The border linewidth (-1) is by default twice as fat as the
   other ones.  Scale appropriately when erasing. *)
let string_of_queue_hide init q =
  Queue.fold (fun cmd (s, lw, lt, color, _) ->
                let lw = if color >= 0 && lt = -1 then 2. *. lw else lw in
                sprintf "%s%s lw %F lt -2," cmd s lw) init q.plots

let ouput_texts gp q =
  Queue.iter (fun (cmd, _) ->
                output_string gp cmd; output_char gp '\n') q.texts

(* [show_plot_queue s_of_q plot gp q] send to [gp] the command to plot
   all the graphs in the queue [q].  [plot] is either "plot " or
   "splot " (note the trailing space).

   REMARK: the linetype "-2" has the color of the background. *)
let show_or_hide_plot_queue s_of_q plot gp q=
  if Queue.is_empty q.plots then force_show gp else begin
    let cmd = s_of_q plot q in
    cmd.[String.length cmd - 1] <- '\n';
    output_string gp cmd;
    (* Inline data *)
    Queue.iter (function
                | (_, _, _, _, Inlined d) -> output_string gp d
                | _ -> ()) q.plots
  end

let show_plot_queue = show_or_hide_plot_queue string_of_queue
let hide_plot_queue = show_or_hide_plot_queue string_of_queue_hide

(* [empty_plot_queue q] frees all data and temp files for queue [q]. *)
let empty_plot_queue q =
  Queue.iter (function
              | (_, _, _, _, File f) -> (try Unix.unlink f with _ -> ())
              | _ -> ()) q.plots;
  Queue.clear q.plots


type plot_history = plots Tags.t

(* [show_plots_all_tags plots gp sp] will send the command to the
   channel [gp] to redraw all the (visible) plots of the subpages [sp]
   starting with no plots.  As we may be in autoscale mode, one must
   group all plots into a _single_ plot instruction to ensure Gnuplot
   sets the ranges correctly (otherwise, several ranges may
   superimpose).  *)
let show_plots_all_tags =
  let add_queue init q =
    match q.state with
    | Managed | Visible -> string_of_queue init q
    | To_show -> (q.state <- Visible; string_of_queue init q)
    | Hidden -> init
    | To_hide -> (q.state <- Hidden; init) in
  fun plot gp history ->
    let cmd = Tags.fold (fun _ q cmd -> add_queue cmd q) history "" in
    if cmd = "" then
      force_show gp (* FIXME: does not work if ranges are not set
                       (see update_show) *)
    else begin
      cmd.[String.length cmd - 1] <- '\n';
      output_string gp plot;
      output_string gp cmd;
      (* Inline data *)
      Tags.iter (fun _ q ->
                   Queue.iter (function
                               | (_, _, _, _, Inlined d) -> output_string gp d
                               | _ -> ()) q.plots
                ) history
    end


(* Maximum number of lines of data for it to be inlined; if the data
   has more than [max_inline] lines, a temporary file is created.  The
   following value has been determined experimentally.  *)
let max_inline = 6

(* The tag for system managed plots.  It will stay in the state
   [Managed] and not be accessible to the user. *)
let system_tag = 0


(* Axes
 **********************************************************************)

(* Axis of a box.  Each axis is independent of the others and consists
   of the following characteristics:
*)
type axis = {
  mutable draw : bool;    (* whether to draw the border *)
  mutable draw_top : bool;(* whether to draw the top border (3D) *)
  mutable zero : bool;    (* whether to draw the zero axis *)
  mutable min : float;    (* [min:max] interval for that axis *)
  mutable max : float;    (* +- infty means autoscale *)
  mutable tics : bool;    (* whether to draw major tics *)
  (* FIXME: tics on zeroaxis??? *)
  mutable tics_step : float; (* <= 0 means automatic *)
  mutable mtics : int;    (* how many minor tics *)
  mutable format : string;(* tics labels format; "" = no label *)
  mutable format_rot : bool; (* true if rotate the label *)
  mutable format_font : string; (* font of tics labels, ex: ["Arial,12"] *)
  mutable format_lt : int; (* gnuplot linetype; color of tics labels *)
  mutable format_color : color; (* color (if >=0, supersedes _lt) *)
  mutable grid : bool;    (* whether a grid is drawn at each major tic *)
  mutable mgrid : bool;   (* whether a grid is drawn at each minor tic *)
  mutable logscale : bool;(* whether to put label logarithmically *)
  mutable active : bool;  (* whether the axis is "active" -- labelled
                             by the *last* [box] command *)
  mutable label : string; (* label for that axis with style (except color),
                             e.g. ["\"x\" font \"Helvetica,default\""]
                             ("\"\"" = none) *)
  mutable label_lt : int; (* the integer is the linetype (gives
                             the color of the text). *)
  mutable label_color : color; (* color of the label; supersede [label_lt]. *)
  mutable label_nlines : int; (* number of lines of the label *)
  mutable label_shown : bool; (* Whether the label has been displayed
                                 (i.e. set + plot) *)
  mutable label_old : string;
  (* The currently _displayed_ label to erase (and to replace with
     [label]).  "" means nothing to erase. *)
}


(* Subpages parameters & basic funtions
 **********************************************************************)

(* Each subpage determines an area where the plotting takes place.

   Unfortunately Gnuplot makes it impossible to correctly set a
   viewport ("box" where the graphs are plotted).  First, margins
   units are chars width and heights (that could be worked around) but
   more importantly they do not obey to fractional sizes (e.g. set
   bmargin 1.4).  As a consequence, one has to let Gnuplot choose the
   viewport.  That implies one must be careful when to redraw the
   entire graph: indeed, each plot command will redraw the
   box,... according to the current parameters -- we must therefore
   keep the entire history.
*)
type subpage = {
  set_subpage : string; (* gnuplot cmd to set the position of the subpage *)
  subpage_xmin : float; (* The coordinates of the subpage; *)
  subpage_ymin : float; (* the dims are hold in the handle since they
                           are the same for all subpages. *)
  mutable redraw : bool;
  (* This flag indicates whether the entire plot must be redrawn. *)

  mutable xmin : float; (* Since the range is set with [win] before [box] *)
  mutable xmax : float; (* can set the active axes, we need a way to store *)
  mutable ymin : float; (* the current coordinates.  +-infty = autoscale *)
  mutable ymax : float;
  x1 : axis; (* bottom *)
  x2 : axis; (* top *)
  y1 : axis; (* left *)
  y2 : axis; (* right *)
  mutable tics_outwards : bool; (* whether tics are drawn outward.
                                   This is global under gnuplot. *)

  mutable title : string; (* title -- see [label] above. *)
  mutable title_nlines : int;
  mutable title_lt : int;
  mutable title_color: color; (* RGB color, if >=0, supersede title_lt *)
  mutable title_shown : bool;
  mutable title_old : string;
  mutable text_number : int; (* the next text number to use for gnuplot
                                in "set label ..." commands. *)
  mutable tags : plot_history; (* history of plotting commands *)

  mutable plot3D : bool; (* says whether the current plot is 3D *)
  mutable zmin : float;
  mutable zmax : float;
  z1 : axis; (* vertical direction for 3D *)
  z2 : axis; (* "front" vertical direction (not everything is used) *)
}


(* [add_plot_with_tag sp t plot] add the plot [plot] to the tag [t] in
   the subpage [sp].  The queue is returned for possible further
   processing.  The tag is created with [Hidden] state if necessary.
   The reasoning for the default state [Hidden] is that tagging plots
   is often used to group them, hence the display should take place
   after all the plots have been tagged.  *)
let add_plot_with_tag sp t plot =
  try
    let q = Tags.find t sp.tags in
    Queue.add plot q.plots;
    q
  with Not_found ->
    let pl = Queue.create() in
    Queue.add plot pl;
    let q = { state = Hidden; plots = pl; texts = Queue.create() } in
    sp.tags <- Tags.add t q sp.tags;
    q


(* [add_text_with_tag sp t text] does the same as [add_plot_with_tag]
   but for text instead of plots. *)
let add_text_with_tag sp t text =
  try
    let q = Tags.find t sp.tags in
    Queue.add text q.texts;
    q
  with Not_found ->
    let txt = Queue.create() in
    Queue.add text txt;
    let q = { state = Hidden; plots = Queue.create(); texts = txt } in
    sp.tags <- Tags.add t q sp.tags;
    q


(* [update_labels gp sp] sends the Gnuplot commands through [gp] to
   carefully erase (immediately) the old the labels and title and
   set the new ones.  This will in 2D and 3D but only work if the
   ranges are set and border, tics,... are unchanged. *)
let update_labels =
  let erz_label gp modif xy a =
    if a.label_old <> "" then begin
      fprintf gp "set %slabel %s tc lt -2;" xy a.label_old;
      modif := true
    end in
  let new_label gp xy a =
    if a.label_old <> "" then begin
      fprintf gp "set %slabel %s tc %s;" xy a.label
        (if a.label_color >= 0 then sprintf "rgb \"#%X\"" a.label_color
         else sprintf "lt %i" a.label_lt);
      a.label_old <- ""
    end in
  fun gp sp ->
    (* Erase old labels *)
    let modif = ref false in
    erz_label gp modif "x" sp.x1;
    erz_label gp modif "x2" sp.x2;
    erz_label gp modif "y" sp.y1;
    erz_label gp modif "y2" sp.y2;
    erz_label gp modif "z" sp.z1;
    if sp.title_old <> "" then begin
      fprintf gp "set title %s tc lt -2;\n" sp.title_old;
      modif := true
    end;
    if !modif then begin
      force_show gp; (* force erasing only if modif to do *)
      (* Set new labels -- for those erased only *)
      new_label gp "x" sp.x1;
      new_label gp "x2" sp.x2;
      new_label gp "y" sp.y1;
      new_label gp "y2" sp.y2;
      new_label gp "z" sp.z1;
      if sp.title_old <> "" then begin
        fprintf gp "set title %s tc %s;\n" sp.title
          (if sp.title_color >= 0 then sprintf "rgb \"#%X\"" sp.title_color
           else sprintf "lt %i" sp.title_lt);
        sp.title_old <- ""
      end
    end

let range_of_float f =
  if is_finite f then string_of_float f else "*"

(* [set_box2D gp sp] sends the Gnuplot commands to [gp] to set the
   box, axes, and title according to the information in the subpage
   [sp].  Only the labels that are UNCHANGED are set, the other ones
   will be set after the old ones are deleted.  This information needs
   to be resent to Gnuplot when one changes subpages for example. *)
let set_box2D gp sp =
  output_string gp (if sp.tics_outwards then "set tics out;"
                    else "set tics in;");
  let borders =
    (if sp.x1.draw then 1 else 0) + (if sp.x2.draw then 4 else 0)
    + (if sp.y1.draw then 2 else 0) + (if sp.y2.draw then 8 else 0) in
  output_string gp ("set border " ^ (string_of_int borders) ^ ";\n");
  let axis xy a =
    let setxy = "set " ^ xy
    and unsetxy = "unset " ^ xy in
    output_string gp ((if a.zero then setxy else unsetxy) ^ "zeroaxis;");
    fprintf gp "set %srange [%s:%s];"
      xy (range_of_float a.min) (range_of_float a.max);
    if a.tics then
      fprintf gp "set %stics border nomirror %srotate %s font \"%s\" tc %s;"
        xy
        (if a.format_rot then "" else "no")
        (if a.tics_step <= 0. then "" else string_of_float a.tics_step)
        a.format_font
        (if a.format_color >= 0 then sprintf "rgb \"#%X\"" a.format_color
         else sprintf "lt %i" a.format_lt)
    else
      output_string gp (unsetxy ^ "tics;");
    if a.mtics > 0 then fprintf gp "set m%stics %i;" xy a.mtics
    else fprintf gp "unset m%stics;\n" xy;
    fprintf gp "set format %s %S;\n" xy a.format;
    fprintf gp "set grid %s%stics %sm%stics front;"
      (if a.grid then "" else "no") xy (if a.mgrid then "" else "no") xy;
    output_string gp ((if a.logscale then "set logscale "
                       else "unset logscale ") ^ xy ^ ";");
    if a.label_old = "" then begin
      fprintf gp "set %slabel %s tc %s;\n" xy a.label
        (if a.label_color >= 0 then sprintf "rgb \"#%X\"" a.label_color
         else sprintf "lt %i" a.label_lt);
      a.label_shown <- true
    end in
  axis "x"  sp.x1;
  axis "x2" sp.x2;
  axis "y"  sp.y1;
  axis "y2" sp.y2;
  if sp.title_old = "" then
    fprintf gp "set title %s tc %s;\n" sp.title
      (if sp.title_color >= 0 then sprintf "rgb \"#%X\"" sp.title_color
       else sprintf "lt %i" sp.title_lt)


(* [set_box3D gp sp] is the same as [set_box2D] but for 3D plots.  *)
let set_box3D gp sp =
  output_string gp (if sp.tics_outwards then "set tics out;"
                    else "set tics in;");
  (* BEWARE: the borders are not linked to axes but to the view; set a
     view so that they initially correspond. *)
  output_string gp "set view 56,43;";
  let borders =
    (if sp.x1.draw then 1 else 0) + (if sp.x2.draw then 8 else 0)
    + (if sp.y1.draw then 2 else 0) + (if sp.y2.draw then 4 else 0)
    + (if sp.x1.draw_top then 1024 else 0)
    + (if sp.x2.draw_top then 512 else 0)
    + (if sp.y1.draw_top then 256 else 0)
    + (if sp.y2.draw_top then 2048 else 0)
    + (if sp.z1.draw then 16 else 0) + (if sp.z1.draw_top then 128 else 0)
    + (if sp.z2.draw then 32 else 0) + (if sp.z2.draw_top then 64 else 0) in
  output_string gp ("set border " ^ (string_of_int borders) ^ ";\n");
  let axis xy a a2 =
    (* let setxy = "set " ^ xy in *)
    let unsetxy = "unset" ^ xy in
    fprintf gp "set %srange [%s:%s];"
      xy (range_of_float a.min) (range_of_float a.max);
    if a.tics then
      fprintf gp "set %stics border %smirror %s font \"%s\" tc %s;"
        xy
        (if a2.tics then "" else "no")
        (if a.tics_step <= 0. then "" else string_of_float a.tics_step)
        a.format_font
        (if a.format_color >= 0 then sprintf "rgb \"#%X\"" a.format_color
         else sprintf "lt %i" a.format_lt)
    else
      output_string gp (unsetxy ^ "tics;");
    if a.mtics > 0 then fprintf gp "set m%stics %i;" xy a.mtics
    else fprintf gp "unset m%stics;" xy;
    fprintf gp "set format %s %S;" xy a.format;
    fprintf gp "set grid %s%stics %sm%stics front;"
      (if a.grid then "" else "no") xy (if a.mgrid then "" else "no") xy;
    output_string gp ((if a.logscale then "set logscale "
                       else "unset logscale ") ^ xy ^ ";");
    if a.label_old = "" then begin
      fprintf gp "set %slabel %s tc %s;\n" xy a.label
        (if a.label_color >= 0 then sprintf "rgb \"#%X\"" a.label_color
         else sprintf "lt %i" a.label_lt);
      a.label_shown <- true
    end in
  axis "x" sp.x1 sp.x2;
  axis "y" sp.y1 sp.y2;
  axis "z" sp.z1 sp.z2;
  if sp.title_old = "" then
    fprintf gp "set title %s tc %s;\n" sp.title
      (if sp.title_color >= 0 then sprintf "rgb \"#%X\"" sp.title_color
       else sprintf "lt %i" sp.title_lt)


(* [is_autoscaled2D sp] says whether an axis is autoscaled.  This is
   important to know whether the entire plot must be redrawn. *)
let is_autoscaled2D =
  let axis a = is_finite a.min && is_finite a.max in
  fun sp ->
    not(axis sp.x1 && axis sp.x2 && axis sp.y1 && axis sp.y2)

let is_autoscaled3D =
  let axis a = is_finite a.min && is_finite a.max in
  fun sp ->
    not(axis sp.x1 && axis sp.x2 && axis sp.y1 && axis sp.y2 && axis sp.z1)

(* [moveto_subpage gp sp] sends the Gnuplot commands through [gp] to
   define the subpage [sp] "area" and reset its parameters -- but does
   NOT clear it; this is the task of the plotting command. *)
let moveto_subpage gp sp =
  output_string gp sp.set_subpage;
  if sp.plot3D then set_box3D gp sp else set_box2D gp sp;
  (* Set subpage labels that must be visible -- the [To_show] will be
     updated later. *)
  output_string gp "unset label;\n";
  Tags.iter (fun  _ q -> match q.state with
             | Managed | Visible -> ouput_texts gp q
             | _  -> ()) sp.tags


(* Gnuplot handle
 **********************************************************************)

type handle = {
  to_gplot : out_channel; (* channel to send commands to gnuplot *)
  gp_extra : Gnuplot_sys.t;
  mutable closed : bool; (* says whether the handle is closed.  FIXME: Every
                            function should check this first. *)
  interactive : bool; (* if true, the commands are issued immediately
                         to gnuplot. *)
  max_inline : int; (* maximum of lines that are inlined (sent through
                       the pipe); if there are more lines, a temp file
                       is created. *)
  buf_inline : Buffer.t; (* buffer to hold the plot loop -- 1 per session *)
  newpage : string; (* gnuplot cmd to be issued to start a new page
                       FIXME: reevaluate whether it is needed. *)

  nxsub : int;  nysub : int;   (* number of subpages in x and y directions *)
  subw : float; subh : float;  (* width and height of subpages *)
  mutable sub : int; (* current subpage number: 0,..., nxsub*nysub - 1 *)
  subpage : subpage array;    (* of length nxsub*nysub *)

  mutable pen : int;           (* Current pen *)
  mutable color : color; (* Current active color (if >=0,
                            otherwise use pen) *)
  mutable pen_width : float;   (* Current pen width *)
  mutable point : int;         (* Current point type *)
  mutable point_width : float; (* Current point width *)
  mutable font : string;       (* Current font ("" = default) *)
  mutable font_size : int;     (* Current font size *)
}


let string_of_font g =
  g.font ^ (if g.font_size <= 0 then ""
            else "," ^ string_of_int g.font_size)



(*
 * Output devices
 **********************************************************************)

type device =
  | X
  | Wxt
  | PS of string   (* Postscript *)
  | EPS of string  (* Encapsulated PostScript *)
  | EPSLaTeX of string (* picture environment including a PS file *)
  | FIG of string  (* Xfig format *)
  | PNG of string  (* Portable Network Graphics *)
  | MP of string   (* Metapost *)
  | MF of string   (* Metafont *)
  | SVG of string  (* Scalable Vector Graphics *)

let device_of_filename file =
  let suffix_is = Filename.check_suffix file in
  if suffix_is ".ps"  then PS(file)
  else if suffix_is ".eps" then EPS(file)
  else if suffix_is ".tex" then EPSLaTeX(file)
  else if suffix_is ".fig" then FIG(file)
  else if suffix_is ".png" then PNG(file)
  else if suffix_is ".mp"  then MP(file)
  else if suffix_is ".mf"  then MF(file)
  else if suffix_is ".svg" then SVG(file)
  else failwith "Gnuplot.device_of_filename"



(*
 * Plots management
 **********************************************************************)

(* See [update_show] for a description.  This function implements the
   main logic for updating plots, labels,...
*)
let update_show_generic sp ~plot_cmd ~redraw ~set_box ?add_plot g =
  (* If we are not in interactive mode, the plots are made at newpages
     so there is never anything to update. *)
  if g.interactive then begin
    if redraw sp then begin
      sp.redraw <- false;
      (* Clear *)
      if g.nxsub = 1 && g.nysub = 1 then output_string g.to_gplot g.newpage
      else output_string g.to_gplot "clear;\n";
      (* All labels and plots have just been erased; mark them as
         such.  Redraw box and all [Managed], [Visible], [To_show]
         plots.  We need to redraw them as a single plot command to
         avoid superimposing (multiplot) multiple ranges (autoscale)
         on the same plot.  *)
      sp.x1.label_old <- "";
      sp.x2.label_old <- "";
      sp.y1.label_old <- "";
      sp.y2.label_old <- "";
      sp.z1.label_old <- "";
      sp.title_old <- "";
      begin match add_plot with
      | Some(t,plot) -> ignore(add_plot_with_tag sp t plot);
      | None -> ()
      end;
      set_box g.to_gplot sp;
      show_plots_all_tags plot_cmd g.to_gplot sp.tags;
      flush g.to_gplot
    end
    else begin
      (* Erase carefully labels and plots *)
      update_labels g.to_gplot sp;
      let del_sth = Tags.fold (fun _ q del -> match q.state with
                               | To_hide ->
                                   hide_plot_queue plot_cmd g.to_gplot q;
                                   q.state <- Hidden;
                                   true
                               | _ -> del) sp.tags false in
      (* Redraw managed plots (in case erasing destroyed them). *)
      if del_sth then
        Tags.iter (fun _ q -> match q.state with
                   | Managed -> show_plot_queue plot_cmd g.to_gplot q
                   | _ -> ()) sp.tags;
      (* Add the new plot (if any) and possibly show it. *)
      begin match add_plot with
      | Some(t, ((s, lw, lt, color, data) as plot)) ->
          let q = add_plot_with_tag sp t plot in
          begin match q.state with
          | Managed | Visible ->
              fprintf g.to_gplot "%s %s lw %F lt %i%s;\n" plot_cmd s lw lt
                (if color >= 0 then sprintf "lc rgb \"#%X\"" color
                 else "");
              (match data with
               | Inlined d -> output_string g.to_gplot d
               | _ -> ())
          | _ -> ()
          end
      | None -> ()
      end;
      (* We also need to process any waiting plots.  However, we do not
         replot the [Visible] plots, they are under user's control. *)
      Tags.iter (fun _ q -> match q.state with
                 | To_show ->
                     show_plot_queue plot_cmd g.to_gplot q;
                     q.state <- Visible
                 | _ -> ()) sp.tags;
      flush g.to_gplot
    end
  end
  else begin
    (* Not interactive mode, just add the plot *)
    match add_plot with
    | Some(t,plot) -> ignore(add_plot_with_tag sp t plot);
    | None -> ()
  end



(* [update_show ?add_plot g] will update the display of the plots.
   That is any plot waiting to be erased or to be shown will be.
   Managed plots may be re-plotted if needed.

   @param add_plot must be a couple (tag, plot).  If present, the plot
   will be added to the tag (the latter will be created if necessary)
   and displayed according to the state of the tag.
*)
let update_show ?add_plot g =
  let sp = g.subpage.(g.sub) in
  if sp.plot3D then
    update_show_generic sp ?add_plot g
      ~plot_cmd:"splot"
      ~redraw:(fun sp -> sp.redraw || is_autoscaled3D sp)
      ~set_box:set_box3D
  else
    update_show_generic sp ?add_plot g
      ~plot_cmd:"plot"
      ~redraw:(fun sp -> sp.redraw || is_autoscaled2D sp)
      ~set_box:set_box2D



(*
 * Gnuplot initializing, closing and (sub)pages
 **********************************************************************)


let inches_per_mm = 1. /. 25.4

let string_of_current_time () =
  let t = Unix.localtime(Unix.time()) in
  sprintf "%4i-%02i-%02i %i:%02i:%02i"
    (1900 + t.Unix.tm_year) (1 + t.Unix.tm_mon) t.Unix.tm_mday
    t.Unix.tm_hour t.Unix.tm_min t.Unix.tm_sec


let init ?offline ?(max_inline=max_inline) ?(persist=true) ?(color=true)
  ?(nxsub=1) ?(nysub=1) ?xsize ?ysize ?(aspect=1.) dev =
  (* Get the sizes *)
  let (xsize, ysize) = match xsize, ysize with
    | Some(x), Some(y) ->
        if x <= 0. then invalid_arg "Gnuplot.init: xsize <= 0"
        else if y <= 0. then invalid_arg "Gnuplot.init: ysize <= 0"
        else (x,y)
    | Some(x), None ->
        if x <= 0. then invalid_arg "Gnuplot.init: xsize <= 0"
        else if aspect <= 0. then invalid_arg "Gnuplot.init: aspect <= 0"
        else (x, aspect *. x)
    | None, Some(y) ->
        if aspect <= 0. then invalid_arg "Gnuplot.init: aspect <= 0"
        else if y <= 0. then invalid_arg "Gnuplot.init: ysize <= 0"
        else (y /. aspect, y)
    | None, None ->
        let x = match dev with
          | X | Wxt | PNG _ | SVG _ -> 550. (* pixels *)
          | PS _ | EPS _ | EPSLaTeX _ | MP _ | MF _ | FIG _ ->
              100. (* milimeters *) in
        if aspect <= 0. then invalid_arg "Gnuplot.init: aspect <= 0"
        else (x, aspect *. x) in
  (* Open a pipe to gnuplot *)
  let (gp, extra), offline = match offline with
    | None -> Gnuplot_sys.open_gnuplot_out persist xsize ysize color, false
    | Some(plt) -> Gnuplot_sys.open_file_out plt, true in
  (* Global settings *)
  if offline then
    fprintf gp "# Generated by OCaml Gnuplot module on %s.\n"
      (string_of_current_time());
  output_string gp "set locale \"C\";\n";
  output_string gp "set encoding iso_8859_1;\n";
  output_string gp "set decimalsign '.';\n";
  output_string gp "set datafile missing \"?\";\n";
  output_string gp "set zero 0.0;\n";
  output_string gp "set hidden3d;\n"; (* FIXME: must be more subtle *)
  (* Set device, output and size *)
  let scolor = if color then "color" else "monochrome" in
  let set_output s = fprintf gp "set output %S;\n" s in
  let w, h, default_font = match dev with
    | X ->
        (* Not setting the terminal amounts to select the default one
           which is what we want for offline output to be able to
           (e.g.) play on windows a script made on Unix. *)
        if not offline then Gnuplot_sys.set_terminal_X gp xsize ysize color;
        (* no need to set output *)
        (1., 1., "default")
    | Wxt ->
        let pgmname = Filename.basename Sys.argv.(0) in
        fprintf gp "set term wxt title \"OCaml Gnuplot: %s\" enhanced \
	  %spersist noraise;\n" pgmname (if persist then "" else "no");
        (1., 1., "default")
    | PS(s) ->
        if xsize >= ysize then begin
          fprintf gp "set terminal postscript landscape enhanced %s;\n" scolor;
          set_output s; (* default size 10x7 inches *)
          (xsize /. 10. *. inches_per_mm, ysize /. 7. *. inches_per_mm,
           "Helvetica")
        end else begin
          fprintf gp "set terminal postscript portrait enhanced %s;\n" scolor;
          set_output s; (* default size 7x10 inches *)
          (xsize /. 7. *. inches_per_mm, ysize /. 10. *. inches_per_mm,
           "Helvetica")
        end
    | EPS(s) ->
        fprintf gp "set terminal postscript eps enhanced %s;\n" scolor;
        set_output s;
        (xsize /. 10. *. inches_per_mm, ysize /. 7. *. inches_per_mm,
         "Helvetica")
    | EPSLaTeX (tex) ->
        let eps = (Filename.chop_suffix tex ".tex") ^ ".eps" in
        fprintf gp "set terminal epslatex %s dashed;\n" scolor;
        set_output eps; (* default size 5x3 inches *)
        (xsize /. 5. *. inches_per_mm, ysize /. 3. *. inches_per_mm,
         "default")
    | FIG(s) ->
        fprintf gp "set terminal fig %s size %.15g %.15g metric dashed \
	  textnormal depth 50;\n"
          scolor (xsize /. 10.) (ysize /. 10.); (* fig: size in centimeters *)
        set_output s;
        (1., 1., "default")
    | PNG(s) ->
        fprintf gp
          "set terminal png transparent medium size %.0f,%0.f enhanced;\n"
          xsize ysize;
        set_output s;
        (1., 1., "medium")
    | MP(s) ->
        fprintf gp "set terminal mp %s dashed tex;\n" scolor;
        set_output s; (* default size 5x3 inches *)
        (xsize /. 5. *. inches_per_mm, ysize /. 3. *. inches_per_mm,
         "cmr10")
    | MF(s) ->
        fprintf gp "set terminal mf;\n";
        set_output s; (* default size 5x3 inches *)
        (xsize /. 5. *. inches_per_mm, ysize /. 3. *. inches_per_mm,
         "cmr10")
    | SVG(s) ->
        fprintf gp "set terminal svg size %F %F dynamic enhanced;\n"
          xsize ysize;
        set_output s;
        (1., 1., "Arial") in
  fprintf gp "set size %.15g,%.15g;\n" w h;
  (* Several plots per page ? *)
  let nxsub = max nxsub 1
  and nysub = max nysub 1 in
  let nsub = nxsub * nysub
  and subw = w /. float nxsub
  and subh = h /. float nysub in
  let begin_new_page =
    sprintf "set multiplot;%sset size %.15g,%.15g;\n"
      (match dev with
       | MP _ | MF _ -> (* Get the size of the output right even if
                           all the subpages are not used. *)
           sprintf "set size %.15g,%.15g; clear;" w h
       | _ -> "")
      subw subh in
  let make_axis () =
    (* BEWARE: this a a mutable structure, so one must create a
       different instance of it for each axis. *)
    {
      draw = false; (* must explicitely ask for axes *)
      draw_top = false;
      zero = false;
      min = neg_infinity; max = infinity; (* autoscale *)
      tics = false;   tics_step = 0.;
      mtics = 0;
      format = "";    format_rot = false;
      format_font = default_font; (* no font size <==> default size *)
      format_lt = -1; (* border color *)
      format_color = -1; (* invalid color (as long as "lt" is possible) *)
      grid = false;   mgrid = false;
      logscale = false;
      active = true;
      label = "\"\""; (* empty label <==> none *)
      label_nlines = 0;
      label_lt = -1; (* border color *)
      label_color = -1; (* invalid color (as long as "lt" is possible) *)
      label_shown = false;
      label_old = ""; (* nothing to erase *)
    } in
  let make_subpage i =
    (* The subpages are counted horizontally, the top left one
       having number [i=0]. *)
    let xmin = float (i mod nxsub) *. subw
    and ymin = float (nysub - 1 - i / nxsub) *. subh in
    {
      subpage_xmin = xmin;
      subpage_ymin = ymin;
      set_subpage = sprintf "set origin %.15g,%.15g;\n" xmin ymin;
      redraw = false;
      xmin = neg_infinity;  xmax = infinity;
      ymin = neg_infinity;  ymax = infinity;
      x1 = make_axis();
      x2 = make_axis();
      y1 = make_axis();
      y2 = make_axis();
      tics_outwards = false;
      title = "\"\"";     (* empty title <==> no title *)
      title_nlines = 0;
      title_lt = -1;  (* color of border *)
      title_color = -1; (* invalid color (as long as "lt" is possible) *)
      title_shown = false;
      title_old = ""; (* no previous title *)
      text_number = 1; (* The lower "label tag" number accepted by gnuplot *)
      tags = Tags.add system_tag { state = Managed;
                                   plots = Queue.create();
                                   texts = Queue.create();
                                 } Tags.empty; (* default queue only *)
      plot3D = false;
      zmin = neg_infinity;  zmax = infinity;
      z1 = make_axis();
      z2 = make_axis();
    } in
  let handle =
    {
      to_gplot = gp;
      gp_extra = extra;
      closed = false;
      interactive = (dev = X || dev = Wxt);
      max_inline = if offline then max_int else max_inline;
      buf_inline = Buffer.create (max_inline * 34);
      newpage = "unset multiplot;\n" ^ begin_new_page;
      (* subpages *)
      nxsub = nxsub; nysub = nysub; subw = subw; subh = subh;
      sub = 0;
      subpage = Array.init nsub make_subpage;
      (* Pen & fonts defaults *)
      pen = -1; (* border color *)
      color = -1; (* invalid color, must be >=0 to be active *)
      pen_width = 1.;
      point = 1;  point_width = 1.;
      font = default_font;
      font_size = 0; (* ==> default *)
    } in
  (* Initialize 1st page *)
  output_string gp begin_new_page;
  (* Prepare the 1st subpage if interactive *)
  if handle.interactive then begin
    moveto_subpage gp handle.subpage.(handle.sub);
    flush gp;
  end;
  (* Return handle *)
  handle


let close g =
  if not g.closed then begin
    g.closed <- true;
    if g.interactive then
      update_show g
    else
      (* Issue the plotting commands for the last page now *)
      Array.iter (fun sp ->
                    moveto_subpage g.to_gplot sp;
                    show_plots_all_tags
                      (if sp.plot3D then "splot " else "plot ")
                      g.to_gplot sp.tags) g.subpage;
    output_string g.to_gplot "unset multiplot;\n";
    flush g.to_gplot;
    try
      Gnuplot_sys.close_out g.to_gplot g.gp_extra
        (* This also removes the temp dir containing the data files. *)
    with Sys_error _ -> failwith "Gnuplot.close"
  end


let adv ?sub g =
  if g.closed then failwith "Gnuplot.adv";
  let nsub = g.nxsub * g.nysub in
  let sub = match sub with
    | None -> g.sub + 1
    | Some i -> if i < 1 then nsub else i - 1 in
  if sub >= nsub then begin
    (* Start a fresh new page at the first subpage *)
    if not g.interactive then
      (* Issue the plotting commands for the current page now *)
      Array.iter (fun sp ->
                    moveto_subpage g.to_gplot sp;
                    show_plots_all_tags
                      (if sp.plot3D then "splot " else "plot ")
                      g.to_gplot sp.tags) g.subpage;
    output_string g.to_gplot g.newpage;
    (* We start with a blank page, so everything is erased. *)
    (* FIXME: restore instead to default (init) state? *)
    Array.iter (fun sp ->
                  sp.x1.label_old <- "";
                  sp.x2.label_old <- "";
                  sp.y1.label_old <- "";
                  sp.y2.label_old <- "";
                  sp.z1.label_old <- "";
                  sp.title_old <- "";
                  Tags.iter (fun _ q -> match q.state with
                             | Managed | To_hide ->
                                 empty_plot_queue q;
                                 q.state <- Hidden
                                   (* FIXME: One may want to replot
                                      tagged plots so keep them ? *)
                             | _ -> ()) sp.tags;
               ) g.subpage;
    g.sub <- 0;
  end else
    g.sub <- sub;
  if g.interactive then
    (* Go to the subpage [sub] and re-set its parameters. *)
    moveto_subpage g.to_gplot g.subpage.(g.sub)


let clear g =
  if g.closed then failwith "Gnuplot.clear";
  if g.interactive then begin
    output_string g.to_gplot "clear;\n";
    flush g.to_gplot
  end;
  let sp = g.subpage.(g.sub) in
  (* All plots in the system_queue are destroyed. *)
  let q = Tags.find system_tag sp.tags in
  empty_plot_queue q;
  (* Update [To_hide] status to [Hidden] *)
  Tags.iter (fun _ q -> match q.state with
             | To_hide ->
                 q.state <- Hidden
             | _ -> ()) sp.tags



(*
 * Tag management
 **********************************************************************)

let tag_manage g fn_name ?tag immediately ~state =
  if g.closed then failwith fn_name;
  begin match tag with
  | None -> ()
  | Some t ->
      if t = system_tag then
        invalid_arg (fn_name ^ ": cannot change the system tag");
      (* Set the tag to the proper To_* state *)
      let sp = g.subpage.(g.sub) in
      try
        let q = Tags.find t sp.tags in
        q.state <- state
      with Not_found ->
        sp.tags <- Tags.add t { state = state;
                                plots = Queue.create();
                                texts = Queue.create();
                              } sp.tags
  end;
  (* Update display -- and possibly process the given tag *)
  if immediately then update_show g


let show ?(immediately=true) ?tag g =
  tag_manage g "Gnuplot.show" ?tag immediately ~state:To_show

let hide ?(immediately=false) ?tag g =
  tag_manage g "Gnuplot.hide" ?tag immediately ~state:To_hide

let auto ~tag g =
  tag_manage g "Gnuplot.auto" ~tag false ~state:Managed

let free ~tag g =
  if g.closed then failwith "Gnuplot.free";
  if tag = system_tag then
    invalid_arg "Gnuplot.free: cannot change the system tag";
  try
    let q = Tags.find tag g.subpage.(g.sub).tags in
    empty_plot_queue q
  with Not_found -> () (* no tag [t], ignore *)


(*
 * Pens, colors and fonts
 **********************************************************************)

let pen g i =
  if g.closed then failwith "Gnuplot.pen: handle closed";
  g.pen <- i

let color g c =
  if g.closed then failwith "Gnuplot.color: handle closed";
  if c < 0 || c > 0xFFFFFF then invalid_arg "Gnuplot.color";
  g.color <- c

let pen_width g w =
  if g.closed then failwith "Gnuplot.pen_width";
  g.pen_width <- w

let point g i =
    if g.closed then failwith "Gnuplot.point";
  g.point <- i

let point_width g w =
  if g.closed then failwith "Gnuplot.point_width";
  g.point_width <- w

let font g fn =
  if g.closed then failwith "Gnuplot.font";
  g.font <- fn

let font_size g sz =
  if g.closed then failwith "Gnuplot.font_size";
  g.font_size <- sz


(*
 * Text
 **********************************************************************)

(* [number_of_lines s] returns the number of lines of [s] as seen by
   Gnuplot for the positioning of the box.  Here are the (a bit odd)
   Gnuplot rules for this.

   -- Any number of "\n" at the beginning is ignored for the frame but
   pushes the title down (possibly in the plot area);

   -- After the first character, all "\n" count, two consecutive "\n"
   or an "\n" as last character imply an empty line;

   -- "" (and thus "\n\n...\n") is equivalent to no title, so 0 lines.

   The rule is therefore the following: ignore newlines at beginning,
   if what remains is "" this count as 0 lines, otherwise you got 1
   line and add one for each newline that follows.
*)
let number_of_lines =
  let rec skip_nl s i =
    if i >= String.length s || String.unsafe_get s i <> '\n' then i
    else skip_nl s (i + 1) in
  let rec count_nl s i acc =
    if i >= String.length s then acc else
      let acc = if String.unsafe_get s i = '\n' then acc + 1 else acc in
      count_nl s (i + 1) acc in
  fun s ->
    let i0 = skip_nl s 0 in
    if i0 = String.length s then 0 else count_nl s (i0 + 1) 1


let change_label g a1 a2 new_label =
  let change a =
    if a.label_shown then begin
      a.label_old <- a.label;
      a.label_shown <- false;
    end;
    let new_nlines = number_of_lines new_label in
    if new_nlines <> a.label_nlines then g.subpage.(g.sub).redraw <- true;
    a.label <- sprintf "%S font \"%s\"" new_label (string_of_font g);
    a.label_lt <- g.pen;
    a.label_color <- g.color;
    a.label_nlines <- new_nlines in
  match a1.active, a2.active with
  | false, true -> change a2
  | _ -> change a1

let xlabel g new_label =
  if g.closed then failwith "Gnuplot.xlabel: handle closed";
  let sp = g.subpage.(g.sub) in
  change_label g sp.x1 sp.x2 new_label

let ylabel g new_label =
  if g.closed then failwith "Gnuplot.ylabel: handle closed";
  let sp = g.subpage.(g.sub) in
  change_label g sp.y1 sp.y2 new_label

let title g new_title =
  if g.closed then failwith "Gnuplot.title: handle closed";
  let sp = g.subpage.(g.sub) in
  if sp.title_shown then begin
    sp.title_old <- sp.title;
    sp.title_shown <- false;
  end;
  let new_nlines = number_of_lines new_title in
  if new_nlines <> sp.title_nlines then sp.redraw <- true;
  sp.title <- sprintf "%S font \"%s\"" new_title (string_of_font g);
  sp.title_lt <- g.pen;
  sp.title_color <- g.color;
  sp.title_nlines <- new_nlines


type coord = Graph | Viewport | Subpage | World

(* FIXME: allow to choose which coordinates *)
(* FIXME: text is the same as a plot. same treatment. *)
let text g ?(tag=system_tag) ?(frame=0.) ?(rotate=0.) ?(coord=Graph) x y s =
  if g.closed then failwith "Gnuplot.text";
  let sp = g.subpage.(g.sub) in
  let pos = match coord with
    | Graph ->
        (* Use the active coordinate system *)
        sprintf "%s %F,%s %F"
          (if sp.x2.active && not sp.x1.active then "second" else "first") x
          (if sp.y2.active && not sp.y1.active then "second" else "first") y
    | Viewport -> sprintf "graph %F,graph %F" x y
    | Subpage ->
        sprintf "screen %F,screen %F"
          (sp.subpage_xmin +. x *. g.subw) (sp.subpage_ymin +. y *. g.subh)
    | World -> sprintf "screen %F,screen %F" x y in
  let text =
    sprintf "set label %i %S at %s rotate by %F font \"%s\" tc %s;\n"
      sp.text_number s pos rotate (string_of_font g)
      (if g.color >= 0 then sprintf "rgb \"#%X\"" g.color
       else sprintf "lt %i" g.pen) in
  let q = add_text_with_tag sp tag (text, sp.text_number) in
  begin match q.state with
  | Managed | Visible -> output_string g.to_gplot text;
  | _ -> ()
  end;
  sp.text_number <- sp.text_number + 1


(*
 * 2D: world coordinates, axes,...
 **********************************************************************)

(* FIXME: aspect ratio? here? *)
(* FIXME: What about aspect ratio?  Do we try to detect it
   automatically or let it be chosen by the user? *)
let win g xmin xmax ymin ymax =
  if g.closed then failwith "Gnuplot.win";
  let sp = g.subpage.(g.sub) in
  sp.xmin <- xmin;
  sp.xmax <- xmax;
  sp.ymin <- ymin;
  sp.ymax <- ymax;
  sp.redraw <- true (* schedule for redrawing which will reissue ranges *)
(*   let aspect = abs_float( ((ymax -. ymin) *. g.subw) *)
(*                           /. ((xmax -. xmin)*. g.subh) ) in *)
(*   if 0.95 <= aspect && aspect <= 1.05 then *)
(*     cmd_issue_and_hist g p "set size ratio -1\n"; (\* FIXME: not ideal *\) *)

type axis_opt =
    handle -> subpage -> float -> float -> axis -> axis -> unit
type border_loc = int list

let axis ?(which=[1]) () _ _ _ _ a1 a2 =
  if List.mem 1 which then a1.zero <- true;
  if List.mem 2 which then a2.zero <- true

let border ?(which=[1;2]) () _ _ _ _ a1 a2 =
  if List.mem 1 which then a1.draw <- true;
  if List.mem 2 which then a2.draw <- true;
  if List.mem 3 which then a1.draw_top <- true;
  if List.mem 4 which then a2.draw_top <- true

let tics ?(which=[1;2]) ?(outward=false) ?(grid=false) ?(minor=0)
  ?(minor_grid=false) ?(log=false) ?(step=0.) () =
  let set a min max =
      a.min <- min;
      a.max <- max;
      a.tics <- true;
      a.tics_step <- step; (* 0. = auto *)
      a.mtics <- minor;
      a.grid <- grid;
      a.mgrid <- minor_grid;
      if log && (min <= 0. || max <= 0.) then
        invalid_arg "Gnuplot.tics";
      a.logscale <- log  in
  fun _ sp min max a1 a2 ->
    if List.mem 1 which then set a1 min max;
    if List.mem 2 which then set a2 min max;
    sp.tics_outwards <- outward

(* FIXME: allows an easy way to put our own label, e.g. dates. *)
let labels ?(which=[1]) ?prec ?(rotate=false) () g _ _ _ a1 a2 =
  let font = string_of_font g in
  let set a =
    a.format <- (match prec with
                 | None -> "%g"
                 | Some p -> "%." ^ (string_of_int p) ^ "g");
    a.format_rot <- rotate;
    a.format_font <- font;
    a.format_lt <- g.pen;
    a.format_color <- g.color;
    a.active <- true in
  if List.mem 1 which then set a1 else a1.active <- false;
  if List.mem 2 which then set a2 else a2.active <- false


let default_opt = [border(); tics(); labels()]

(* REMARK: The default linetype is 1.  When axis labels are given this
   color, gnuplot will in fact display them with border color.  This
   is a bit strange but does not really interfere -- may be surprising
   to the user though.

   [box] cannot reset the axes to a default state before applying the
   options because we want incremental changes to be possible.

   FIXME: need a way to reset these parameters (e.g. labels, once set
   cannot be unset).  Do it in [clear] ? *)
let box ?(x=default_opt) ?(y=default_opt) g =
  if g.closed then failwith "Gnuplot.box";
  let sp = g.subpage.(g.sub) in
  List.iter (fun prop -> prop g sp sp.xmin sp.xmax sp.x1 sp.x2) x;
  List.iter (fun prop -> prop g sp sp.ymin sp.ymax sp.y1 sp.y2) y;
  sp.redraw <- true (* only matters for interactive mode *)


let env g ?(xaxis=false) ?(xgrid=false) ?(xlog=false) xmin xmax
  ?(yaxis=false) ?(ygrid=false) ?(ylog=false) ymin ymax =
  if g.closed then failwith "Gnuplot.env";
  win g xmin xmax ymin ymax;
  let x = [border(); tics ~grid:xgrid ~log:xlog (); labels()] in
  let x = if xaxis then axis() :: x else x in
  let y = [border(); tics ~grid:ygrid ~log:ylog (); labels()] in
  let y = if yaxis then axis() :: y else y in
  box g ~x ~y



(*
 * Generic plotting functions
 **********************************************************************)

type style = Lines | Linespoints | Points | Dots | Impulses

let with_style g = function
  | Lines -> "with lines"
  | Linespoints ->
      sprintf "with linespoints pt %i ps %F" g.point g.point_width
  | Points ->
      sprintf "with points pt %i ps %g" g.point g.point_width
  | Dots -> "with dots"
  | Impulses -> "with impulses"

let with_vector = "with vector"

(* Active axes for 'plot' *)
let axes g =
  let sp = g.subpage.(g.sub) in
  "axes " ^
  (if sp.x2.active && not sp.x1.active then "x2" else "x1") ^
  (if sp.y2.active && not sp.y1.active then "y2 " else "y1 ")

(* 'splot' does not support 'axes' *)
let no_axes _ = ""

(* REMARK: the callback, calling [loop] with the output command as
   argument, is even a bit faster than the loop with this command
   inlined! *)
let plot_data g ?(tag=system_tag) ~inline_if loop ~axes ~label ~style =
  (* The border linetype (-1) is twice as thick as the other ones.  So
     scale the size appropriately for visual homogeneity.  *)
  let lw = (if g.color < 0 && g.pen = -1 then
              0.5 *. g.pen_width else g.pen_width) in
  let plot =
    if inline_if then begin
      Buffer.reset g.buf_inline;
      try
        let () = loop(Buffer.add_string g.buf_inline) in
        Buffer.add_string g.buf_inline "e\n";
        let data = Buffer.contents g.buf_inline in
        (* Plot command *)
        let cmd = sprintf "'-' %s title %S %s" (axes g) label style in
        (cmd, lw, g.pen, g.color, Inlined(data))
      with e ->
        Buffer.reset g.buf_inline;
        raise e
    end
    else begin
      let (fname, fh) = Gnuplot_sys.open_temp_file g.gp_extra in
      try
        loop(output_string fh);
        close_out fh;
        (* Plot command *)
        let cmd =
          sprintf "%S %s title %S %s" fname (axes g) label style in
        (cmd, lw, g.pen, g.color, File fname)
      with e ->
        close_out fh;
        (try Unix.unlink fname with _ -> ());
        raise e
    end in
  (* Add plot and possibly display it *)
  update_show g ~add_plot:(tag, plot)



(*
 * 2D Plots
 **********************************************************************)

let fx g ?tag ?(style=Lines) ?(label="") ?(nsamples=100) f a b =
  if g.closed then failwith "Gnuplot.fx";
  if not(is_finite a) || not(is_finite b) || nsamples <= 1 then
    invalid_arg "Gnuplot.fx";
  let loop out =
    let nsamples = nsamples - 1 in
    let h = (b -. a) /. float nsamples in
    for i = 0 to nsamples do
      let x = a +. float i *. h in
      let y = f x in
      if is_finite y then
        out(string_of_float x ^ " " ^ string_of_float y ^ "\n")
      else
        out "? ?\n"
    done in
  plot_data g ~inline_if:(nsamples <= g.max_inline)
    loop ?tag ~axes ~label ~style:(with_style g style)



let xy_param g ?tag ?(style=Lines) ?(label="") ?(nsamples=100) f a b =
  if g.closed then failwith "Gnuplot.xy_param";
  if nsamples <= 1 then invalid_arg "Gnuplot.xy_param";
  let loop out =
    let nsamples = nsamples - 1 in
    let h = (b -. a) /. float nsamples in
    for i = 0 to nsamples do
      let x = a +. float i *. h in
      let (y1, y2) = f x in
      if is_finite y1 && is_finite y2 then begin
        out(string_of_float y1 ^ " " ^ string_of_float y2 ^ "\n")
      end else
        out "? ?\n"
    done in
  plot_data g ~inline_if:(nsamples <= g.max_inline)
    loop ?tag ~axes ~label ~style:(with_style g style)


(* This is similar to the second case of [plot_data] except the file
   is already created. *)
let xy_file g ?(tag=system_tag) ?(style=Lines) ?(label="") fname =
  if g.closed then failwith "Gnuplot.xy_file";
  let lw = if g.pen = -1 then 0.5 *. g.pen_width else g.pen_width in
  let cmd = sprintf "%S title %S %s %s"
              fname label (axes g) (with_style g style) in
  update_show g ~add_plot:(tag, (cmd, lw, g.pen, g.color, File fname))


(*
 * 3D: world coordinates, axes,...
 **********************************************************************)
(* FIXME: 3D plots are in alpha state *)

let win3 g xmin xmax ymin ymax zmin zmax =
  if g.closed then failwith "Gnuplot.win3";
  let sp = g.subpage.(g.sub) in
  sp.xmin <- xmin;
  sp.xmax <- xmax;
  sp.ymin <- ymin;
  sp.ymax <- ymax;
  sp.zmin <- zmin;
  sp.zmax <- zmax;
  sp.plot3D <- true;
  sp.redraw <- true

let default_opt3d = [border(); tics ~minor:0 (); labels()]
let default_opt3d_z = [border ~which:[1] ();
                       tics ~which:[1] ~minor:0 ();
                       labels()]

let box3 ?(x=default_opt3d) ?(y=default_opt3d) ?(z=default_opt3d_z) g =
  if g.closed then failwith "Gnuplot.box3";
  let sp = g.subpage.(g.sub) in
  List.iter (fun prop -> prop g sp sp.xmin sp.xmax sp.x1 sp.x2) x;
  List.iter (fun prop -> prop g sp sp.ymin sp.ymax sp.y1 sp.y2) y;
  List.iter (fun prop -> prop g sp sp.zmin sp.zmax sp.z1 sp.z2) z;
  sp.plot3D <- true;
  sp.redraw <- true

let env3 g ?(xaxis=false) ?(xgrid=false) ?(xlog=false) xmin xmax
  ?(yaxis=false) ?(ygrid=false) ?(ylog=false) ymin ymax
  ?(zaxis=false) ?(zgrid=false) ?(zlog=false) zmin zmax =
  if g.closed then failwith "Gnuplot.env3";
  win3 g xmin xmax ymin ymax zmin zmax;
  let x = [border(); tics ~minor:0 ~grid:xgrid ~log:xlog (); labels()] in
  let x = if xaxis then axis() :: x else x in
  let y = [border(); tics ~minor:0 ~grid:ygrid ~log:ylog (); labels()] in
  let y = if yaxis then axis() :: y else y in
  let z = [border ~which:[1] ();
           tics ~which:[1] ~minor:0 ~grid:ygrid ~log:ylog (); labels()] in
  let z = if zaxis then axis() :: z else z in
  box3 g ~x ~y ~z


(*
 * 3D Plots
 **********************************************************************)

(* FIXME: hidden -- global? *)
let fxy g ?tag ?(style=Lines) ?(label="")
  ?(xnsamples=30) ?(ynsamples=30) f xmin xmax ymin ymax =
  if g.closed then failwith "Gnuplot.fxy";
  if not(is_finite xmin && is_finite xmax && is_finite ymin
         && is_finite ymax) || xnsamples <= 1 || ynsamples <= 1 then
    invalid_arg "Gnuplot.fxy";
  let loop out =
    let xnsamples = xnsamples - 1
    and ynsamples = ynsamples - 1 in
    let hx = (xmax -. xmin) /. float xnsamples
    and hy = (ymax -. ymin) /. float ynsamples in
    for i = 0 to xnsamples do
      let x = xmin +. float i *. hx in
      for j = 0 to ynsamples do
        let y = ymin +. float j *. hy in
        let z = f x y in
        if is_finite z then out (sprintf "%F %F %F\n" x y z)
        else out (sprintf "%F %F ?\n" x y)
      done;
      out "\n";
    done in
  plot_data g ~inline_if:(xnsamples * ynsamples <= g.max_inline)
    loop ?tag ~axes:no_axes ~label ~style:(with_style g style)

let fxy_param g ?tag ?(style=Lines) ?(label="")
  ?(xnsamples=30) ?(ynsamples=30) f xmin xmax ymin ymax =
  if g.closed then failwith "Gnuplot.fxy_param";
  if xnsamples <= 1 || ynsamples <= 1 then invalid_arg "Gnuplot.fxy_param";
  let loop out =
    let xnsamples = xnsamples - 1
    and ynsamples = ynsamples - 1 in
    let hx = (xmax -. xmin) /. float xnsamples
    and hy = (ymax -. ymin) /. float ynsamples in
    for i = 0 to xnsamples do
      let x = xmin +. float i *. hx in
      for j = 0 to ynsamples do
        let y = ymin +. float j *. hy in
        let (z1, z2, z3) = f x y in
        if is_finite z1 && is_finite z2 && is_finite z3 then
          out (sprintf "%F %F %F\n" z1 z2 z3)
        else out "? ? ?\n"
      done;
      out "\n";
    done in
  plot_data g ~inline_if:(xnsamples * ynsamples <= g.max_inline)
    loop ?tag ~axes:no_axes ~label ~style:(with_style g style)


let xyz_ft g ?tag ?(style=Lines) ?(label="") ?(tnsamples=100) f tmin tmax =
  if g.closed then failwith "Gnuplot.xyz_ft";
  if tnsamples <= 1 then invalid_arg "Gnuplot.xyz_ft";
  let loop out =
    let tnsamples = tnsamples - 1 in
    let ht = (tmax -. tmin) /. float tnsamples in
    for i = 0 to tnsamples do
      let (z1, z2, z3) = f(tmin +. float i *. ht) in
      if is_finite z1 && is_finite z2 && is_finite z3 then
        out (sprintf "%F %F %F\n" z1 z2 z3)
      else out "? ? ?\n"
    done in
  plot_data g ~inline_if:(tnsamples <= g.max_inline)
    loop ?tag ~axes:no_axes ~label ~style:(with_style g style)