config

Personal configuration.
git clone git://code.dwrz.net/config
Log | Files | Refs

org-plot.el (27834B)


      1 ;;; org-plot.el --- Support for Plotting from Org -*- lexical-binding: t; -*-
      2 
      3 ;; Copyright (C) 2008-2024 Free Software Foundation, Inc.
      4 ;;
      5 ;; Author: Eric Schulte <schulte dot eric at gmail dot com>
      6 ;; Maintainer: TEC <orgmode@tec.tecosaur.net>
      7 ;; Keywords: tables, plotting
      8 ;; URL: https://orgmode.org
      9 ;;
     10 ;; This file is part of GNU Emacs.
     11 ;;
     12 ;; GNU Emacs is free software: you can redistribute it and/or modify
     13 ;; it under the terms of the GNU General Public License as published by
     14 ;; the Free Software Foundation, either version 3 of the License, or
     15 ;; (at your option) any later version.
     16 
     17 ;; GNU Emacs is distributed in the hope that it will be useful,
     18 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     19 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     20 ;; GNU General Public License for more details.
     21 
     22 ;; You should have received a copy of the GNU General Public License
     23 ;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
     24 
     25 ;;; Commentary:
     26 
     27 ;; Borrows ideas and a couple of lines of code from org-exp.el.
     28 
     29 ;; Thanks to the Org mailing list for testing and implementation and
     30 ;; feature suggestions
     31 
     32 ;;; Code:
     33 
     34 (require 'org-macs)
     35 (org-assert-version)
     36 
     37 (require 'cl-lib)
     38 (require 'org)
     39 (require 'org-table)
     40 
     41 (declare-function gnuplot-delchar-or-maybe-eof "ext:gnuplot" (arg))
     42 (declare-function gnuplot-mode "ext:gnuplot" ())
     43 (declare-function gnuplot-send-buffer-to-gnuplot "ext:gnuplot" ())
     44 
     45 (defvar org-plot/gnuplot-default-options
     46   '((:plot-type . 2d)
     47     (:with . lines)
     48     (:ind . 0))
     49   "Default options to gnuplot used by `org-plot/gnuplot'.")
     50 
     51 (defvar org-plot-timestamp-fmt nil)
     52 
     53 (defun org-plot/add-options-to-plist (p options)
     54   "Parse an OPTIONS line and set values in the property list P.
     55 Returns the resulting property list."
     56   (when options
     57     (let ((op '(("type"      . :plot-type)
     58 		("script"    . :script)
     59 		("line"      . :line)
     60 		("set"       . :set)
     61 		("title"     . :title)
     62 		("ind"       . :ind)
     63 		("deps"      . :deps)
     64 		("with"      . :with)
     65 		("file"      . :file)
     66 		("labels"    . :labels)
     67 		("map"       . :map)
     68 		("timeind"   . :timeind)
     69 		("timefmt"   . :timefmt)
     70 		("min"       . :ymin)
     71 		("ymin"      . :ymin)
     72 		("max"       . :ymax)
     73 		("ymax"      . :ymax)
     74 		("xmin"      . :xmin)
     75 		("xmax"      . :xmax)
     76 		("ticks"     . :ticks)
     77 		("trans"     . :transpose)
     78 		("transpose" . :transpose)))
     79 	  (multiples '("set" "line"))
     80 	  (regexp ":\\([\"][^\"]+?[\"]\\|[(][^)]+?[)]\\|[^ \t\n\r;,.]*\\)")
     81 	  (start 0))
     82       (dolist (o op)
     83 	(if (member (car o) multiples) ;; keys with multiple values
     84 	    (while (string-match
     85 		    (concat (regexp-quote (car o)) regexp)
     86 		    options start)
     87 	      (setq start (match-end 0))
     88 	      (setq p (plist-put p (cdr o)
     89 				 (cons (car (read-from-string
     90 					     (match-string 1 options)))
     91 				       (plist-get p (cdr o)))))
     92 	      p)
     93 	  (if (string-match (concat (regexp-quote (car o)) regexp)
     94 			    options)
     95 	      (setq p (plist-put p (cdr o)
     96 				 (car (read-from-string
     97 				       (match-string 1 options))))))))))
     98   p)
     99 
    100 (defun org-plot/goto-nearest-table ()
    101   "Move the point forward to the beginning of nearest table.
    102 Return value is the point at the beginning of the table."
    103   (interactive) (move-beginning-of-line 1)
    104   (while (not (or (org-at-table-p) (< 0 (forward-line 1)))))
    105   (goto-char (org-table-begin)))
    106 
    107 (defun org-plot/collect-options (&optional params)
    108   "Collect options from an org-plot `#+Plot:' line.
    109 Accepts an optional property list PARAMS, to which the options
    110 will be added.  Returns the resulting property list."
    111   (interactive)
    112   (let ((line (thing-at-point 'line)))
    113     (if (string-match "#\\+PLOT: +\\(.*\\)$" line)
    114 	(org-plot/add-options-to-plist params (match-string 1 line))
    115       params)))
    116 
    117 (defun org-plot-quote-timestamp-field (s)
    118   "Convert field S from timestamp to Unix time and export to gnuplot."
    119   (format-time-string org-plot-timestamp-fmt (org-time-string-to-time s)))
    120 
    121 (defun org-plot-quote-tsv-field (s)
    122   "Quote field S for export to gnuplot."
    123   (if (string-match org-table-number-regexp s) s
    124     (if (string-match org-ts-regexp3 s)
    125 	(org-plot-quote-timestamp-field s)
    126       (concat "\"" (mapconcat 'identity (split-string s "\"") "\"\"") "\""))))
    127 
    128 (defun org-plot/gnuplot-to-data (table data-file params)
    129   "Export TABLE to DATA-FILE in a format readable by gnuplot.
    130 Pass PARAMS through to `orgtbl-to-generic' when exporting TABLE."
    131   (with-temp-file
    132       data-file
    133     (setq-local org-plot-timestamp-fmt (or
    134 					(plist-get params :timefmt)
    135 					"%Y-%m-%d-%H:%M:%S"))
    136     (insert (orgtbl-to-generic
    137 	     table
    138 	     (org-combine-plists
    139 	      '(:sep "\t" :fmt org-plot-quote-tsv-field)
    140 	      params))))
    141   nil)
    142 
    143 (defun org-plot/gnuplot-to-grid-data (table data-file params)
    144   "Export the data in TABLE to DATA-FILE for gnuplot.
    145 This means in a format appropriate for grid plotting by gnuplot.
    146 PARAMS specifies which columns of TABLE should be plotted as independent
    147 and dependent variables."
    148   (interactive)
    149   (let* ((ind (- (plist-get params :ind) 1))
    150 	 (deps (if (plist-member params :deps)
    151 		   (mapcar (lambda (val) (- val 1)) (plist-get params :deps))
    152 		 (let (collector)
    153 		   (dotimes (col (length (nth 0 table)))
    154 		     (setf collector (cons col collector)))
    155 		   collector)))
    156 	 (counter 0)
    157 	 row-vals)
    158     (when (>= ind 0) ;; collect values of ind col
    159       (setf row-vals (mapcar (lambda (row) (setf counter (+ 1 counter))
    160 			       (cons counter (nth ind row)))
    161 			     table)))
    162     (when (or deps (>= ind 0)) ;; remove non-plotting columns
    163       (setf deps (delq ind deps))
    164       (setf table (mapcar (lambda (row)
    165 			    (dotimes (col (length row))
    166 			      (unless (memq col deps)
    167 				(setf (nth col row) nil)))
    168 			    (delq nil row))
    169 			  table)))
    170     ;; write table to gnuplot grid datafile format
    171     (with-temp-file data-file
    172       (let ((num-rows (length table)) (num-cols (length (nth 0 table)))
    173 	    (gnuplot-row (lambda (col row value)
    174 			   (setf col (+ 1 col)) (setf row (+ 1 row))
    175 			   (format "%f  %f  %f\n%f  %f  %f\n"
    176 				   col (- row 0.5) value ;; lower edge
    177 				   col (+ row 0.5) value))) ;; upper edge
    178 	    front-edge back-edge)
    179 	(dotimes (col num-cols)
    180 	  (dotimes (row num-rows)
    181 	    (setf back-edge
    182 		  (concat back-edge
    183 			  (funcall gnuplot-row (- col 1) row
    184 				   (string-to-number (nth col (nth row table))))))
    185 	    (setf front-edge
    186 		  (concat front-edge
    187 			  (funcall gnuplot-row col row
    188 				   (string-to-number (nth col (nth row table)))))))
    189 	  ;; only insert once per row
    190 	  (insert back-edge) (insert "\n") ;; back edge
    191 	  (insert front-edge) (insert "\n") ;; front edge
    192 	  (setf back-edge "") (setf front-edge ""))))
    193     row-vals))
    194 
    195 (defun org--plot/values-stats (nums &optional hard-min hard-max)
    196   "Rudimentary statistics about NUMS, useful for guessing axis ticks.
    197 If HARD-MIN or HARD-MAX are set, they will be used instead of the min/max
    198 of the NUMS."
    199   (let* ((minimum (or hard-min (apply #'min nums)))
    200 	 (maximum (or hard-max (apply #'max nums)))
    201 	 (range (- maximum minimum))
    202 	 (rangeOrder (if (= range 0) 0
    203 		       (ceiling (- 1 (log range 10)))))
    204 	 (range-factor (expt 10 rangeOrder))
    205 	 (nice-min (if (= range 0) (car nums)
    206 		     (/ (float (floor (* minimum range-factor))) range-factor)))
    207 	 (nice-max (if (= range 0) (car nums)
    208 		     (/ (float (ceiling (* maximum range-factor))) range-factor))))
    209     `(:min ,minimum :max ,maximum :range ,range
    210            :range-factor ,range-factor
    211            :nice-min ,nice-min :nice-max ,nice-max :nice-range ,(- nice-max nice-min))))
    212 
    213 (defun org--plot/sensible-tick-num (table &optional hard-min hard-max)
    214   "From a the values in a TABLE of data, guess an appropriate number of ticks.
    215 If HARD-MIN and HARD-MAX can be used to fix the ends of the axis."
    216   (let* ((row-data
    217 	  (mapcar (lambda (row) (org--plot/values-stats
    218 			         (mapcar #'string-to-number (cdr row))
    219 			         hard-min
    220 			         hard-max)) table))
    221 	 (row-normalised-ranges (mapcar (lambda (r-data)
    222 					  (let ((val (round (*
    223 							     (plist-get r-data :range-factor)
    224 							     (plist-get r-data :nice-range)))))
    225 					    (if (= (% val 10) 0) (/ val 10) val)))
    226 					row-data))
    227 	 (range-prime-decomposition (mapcar #'org--plot/prime-factors row-normalised-ranges))
    228 	 (weighted-factors (sort (apply #'org--plot/merge-alists #'+ 0
    229 					(mapcar (lambda (factors) (org--plot/item-frequencies factors t))
    230 						range-prime-decomposition))
    231 				 (lambda (a b) (> (cdr a) (cdr b))))))
    232     (apply #'* (org--plot/nice-frequency-pick weighted-factors))))
    233 
    234 (defun org--plot/nice-frequency-pick (frequencies)
    235   "From a list of FREQUENCIES, try to sensibly pick a sample of the most frequent."
    236   ;; TODO this mosly works decently, but could do with some tweaking to work more consistently.
    237   (cl-case (length frequencies)
    238     (1 (list (car (nth 0 frequencies))))
    239     (2 (if (<= 3 (/ (cdr (nth 0 frequencies))
    240 		    (cdr (nth 1 frequencies))))
    241 	   (make-list 2
    242 		      (car (nth 0 frequencies)))
    243 	 (list (car (nth 0 frequencies))
    244 	       (car (nth 1 frequencies)))))
    245     (t
    246      (let* ((total-count (apply #'+ (mapcar #'cdr frequencies)))
    247 	    (n-freq (mapcar (lambda (freq) `(,(car freq) . ,(/ (float (cdr freq)) total-count))) frequencies))
    248 	    (f-pick (list (car (car n-freq))))
    249 	    (1-2-ratio (/ (cdr (nth 0 n-freq))
    250 			  (cdr (nth 1 n-freq))))
    251 	    (2-3-ratio (/ (cdr (nth 1 n-freq))
    252 			  (cdr (nth 2 n-freq))))
    253 	    (1-3-ratio (* 1-2-ratio 2-3-ratio))
    254 	    (1-val (car (nth 0 n-freq)))
    255 	    (2-val (car (nth 1 n-freq)))
    256 	    (3-val (car (nth 2 n-freq))))
    257        (when (> 1-2-ratio 4) (push 1-val f-pick))
    258        (when (and (< 1-2-ratio 2-val)
    259 		  (< (* (apply #'* f-pick) 2-val) 30))
    260 	 (push 2-val f-pick))
    261        (when (and (< 1-3-ratio 3-val)
    262 		  (< (* (apply #'* f-pick) 3-val) 30))
    263 	 (push 3-val f-pick))
    264        f-pick))))
    265 
    266 (defun org--plot/merge-alists (function default alist1 alist2 &rest alists)
    267   "Using FUNCTION, combine the elements of ALIST1, ALIST2 and any other ALISTS.
    268 When an element is only present in one alist, DEFAULT is used as the second
    269 argument for the FUNCTION."
    270   (when (> (length alists) 0)
    271     (setq alist2 (apply #'org--plot/merge-alists function default alist2 alists)))
    272   (cl-flet ((keys (alist) (mapcar #'car alist))
    273 	    (lookup (key alist) (or (cdr (assoc key alist)) default)))
    274     (cl-loop with keys = (cl-union (keys alist1) (keys alist2) :test 'equal)
    275 	     for k in keys collect
    276 	     (cons k (funcall function (lookup k alist1) (lookup k alist2))))))
    277 
    278 (defun org--plot/item-frequencies (values &optional normalize)
    279   "Return an alist indicating the frequency of values in VALUES list.
    280 When NORMALIZE is non-nil, the count is divided by the number of values."
    281   (let ((normaliser (if normalize (float (length values)) 1)))
    282     (cl-loop for (n . m) in (seq-group-by #'identity values)
    283 	     collect (cons n (/ (length m) normaliser)))))
    284 
    285 (defun org--plot/prime-factors (value)
    286   "Return the prime decomposition of VALUE, e.g. for 12, (3 2 2)."
    287   (let ((factors '(1)) (i 1))
    288     (while (/= 1 value)
    289       (setq i (1+ i))
    290       (when (eq 0 (% value i))
    291 	(push i factors)
    292 	(setq value (/ value i))
    293 	(setq i (1- i))
    294 	))
    295     (cl-subseq factors 0 -1)))
    296 
    297 (defgroup org-plot nil
    298   "Options for plotting in Org mode."
    299   :tag "Org Plot"
    300   :group 'org)
    301 
    302 (defcustom org-plot/gnuplot-script-preamble ""
    303   "String of function to be inserted before the gnuplot plot command is run.
    304 
    305 Note that this is in addition to, not instead of other content generated in
    306 `org-plot/gnuplot-script'.  If a function, it is called with the plot type as
    307 the argument, and must return a string to be used."
    308   :group 'org-plot
    309   :type '(choice string function))
    310 
    311 (defcustom org-plot/preset-plot-types
    312   '((2d :plot-cmd "plot"
    313 	:check-ind-type t
    314 	:plot-func
    315 	(lambda (_table data-file num-cols params plot-str)
    316 	  (let* ((type (plist-get params :plot-type))
    317 		 (with (if (eq type 'grid) 'pm3d (plist-get params :with)))
    318 		 (ind (plist-get params :ind))
    319 		 (deps (if (plist-member params :deps) (plist-get params :deps)))
    320 		 (text-ind (or (plist-get params :textind)
    321                                (eq (plist-get params :with) 'histograms)))
    322 		 (col-labels (plist-get params :labels))
    323 		 res)
    324 	    (dotimes (col num-cols res)
    325 	      (unless (and (eq type '2d)
    326 			   (or (and ind (equal (1+ col) ind))
    327 			       (and deps (not (member (1+ col) deps)))))
    328 		(setf res
    329 		      (cons
    330 		       (format plot-str data-file
    331 			       (or (and ind (> ind 0)
    332 					(not text-ind)
    333 					(format "%d:" ind)) "")
    334 			       (1+ col)
    335 			       (if text-ind (format ":xticlabel(%d)" ind) "")
    336 			       with
    337 			       (or (nth col col-labels)
    338 				   (format "%d" (1+ col))))
    339 		       res)))))))
    340     (3d :plot-cmd "splot"
    341 	:plot-pre (lambda (_table _data-file _num-cols params _plot-str)
    342 		    (if (plist-get params :map) "set map"))
    343 	:plot-func
    344 	(lambda (_table data-file _num-cols params _plot-str)
    345 	  (let* ((type (plist-get params :plot-type))
    346 		 (with (if (eq type 'grid) 'pm3d (plist-get params :with))))
    347 	    (list (format "'%s' matrix with %s title ''"
    348 			  data-file with)))))
    349     (grid :plot-cmd "splot"
    350 	  :plot-pre (lambda (_table _data-file _num-cols params _plot-str)
    351 		      (if (plist-get params :map) "set pm3d map" "set map"))
    352 	  :data-dump (lambda (table data-file params _num-cols)
    353 		       (let ((y-labels (org-plot/gnuplot-to-grid-data
    354 					table data-file params)))
    355 			 (when y-labels (plist-put params :ylabels y-labels))))
    356 	  :plot-func
    357 	  (lambda (table data-file _num-cols params _plot-str)
    358 	    (let* ((type (plist-get params :plot-type))
    359 		   (with (if (eq type 'grid) 'pm3d (plist-get params :with))))
    360 	      (list (format "'%s' with %s title ''"
    361 			    data-file with)))))
    362     (radar :plot-func
    363 	   (lambda (table _data-file _num-cols params plot-str)
    364 	     (list (org--plot/radar table params)))))
    365   "List of plists describing the available plot types.
    366 The car is the type name, and the property :plot-func must be
    367 set.  The value of :plot-func is a lambda which yields plot-lines
    368 \(a list of strings) as the cdr.
    369 
    370 All lambda functions have the parameters of
    371 `org-plot/gnuplot-script' and PLOT-STR passed to them.  i.e. they
    372 are called with the following signature: (TABLE DATA-FILE
    373 NUM-COLS PARAMS PLOT-STR)
    374 
    375 Potentially useful parameters in PARAMS include:
    376  :set :line :map :title :file :ind :timeind :timefmt :textind
    377  :deps :labels :xlabels :ylabels :xmin :xmax :ymin :ymax :ticks
    378 
    379 In addition to :plot-func, the following optional properties may
    380 be set.
    381 
    382 - :plot-cmd - A gnuplot command appended to each plot-line.
    383   Accepts string or nil.  Default value: nil.
    384 
    385 - :check-ind-type - Whether the types of ind values should be checked.
    386   Accepts boolean.
    387 
    388 - :plot-str - the formula string passed to :plot-func as PLOT-STR
    389   Accepts string.  Default value: \"'%s' using %s%d%s with %s title '%s'\"
    390 
    391 - :data-dump - Function to dump the table to a datafile for ease of
    392   use.
    393 
    394   Accepts lambda function.  Default lambda body:
    395   (org-plot/gnuplot-to-data table data-file params)
    396 
    397 - :plot-pre - Gnuplot code to be inserted early into the script, just
    398   after term and output have been set.
    399 
    400    Accepts string, nil, or lambda function which returns string
    401    or nil.  Defaults to nil."
    402   :group 'org-plot
    403   :type 'alist)
    404 
    405 (defvar org--plot/radar-template
    406   "### spider plot/chart with gnuplot
    407 # also known as: radar chart, web chart, star chart, cobweb chart,
    408 #                radar plot,  web plot,  star plot,  cobweb plot,  etc. ...
    409 set datafile separator ' '
    410 set size square
    411 unset tics
    412 set angles degree
    413 set key bmargin center horizontal
    414 unset border
    415 
    416 # Load data and setup
    417 load \"%s\"
    418 
    419 # General settings
    420 DataColCount = words($Data[1])-1
    421 AxesCount = |$Data|-HeaderLines-1
    422 AngleOffset = 90
    423 Max = 1
    424 d=0.1*Max
    425 Direction = -1   # counterclockwise=1, clockwise = -1
    426 
    427 # Tic settings
    428 TicCount = %s
    429 TicOffset = 0.1
    430 TicValue(axis,i) = real(i)*(word($Settings[axis],3)-word($Settings[axis],2)) \\
    431 	  / word($Settings[axis],4)+word($Settings[axis],2)
    432 TicLabelPosX(axis,i) = PosX(axis,i/TicCount) + PosY(axis, TicOffset)
    433 TicLabelPosY(axis,i) = PosY(axis,i/TicCount) - PosX(axis, TicOffset)
    434 TicLen = 0.03
    435 TicdX(axis,i) = 0.5*TicLen*cos(alpha(axis)-90)
    436 TicdY(axis,i) = 0.5*TicLen*sin(alpha(axis)-90)
    437 
    438 # Label
    439 LabOffset = 0.10
    440 LabX(axis) = PosX(axis+1,Max+2*d) + PosY(axis, LabOffset)
    441 LabY(axis) = PosY($0+1,Max+2*d)
    442 
    443 # Functions
    444 alpha(axis) = (axis-1)*Direction*360.0/AxesCount+AngleOffset
    445 PosX(axis,R) = R*cos(alpha(axis))
    446 PosY(axis,R) = R*sin(alpha(axis))
    447 Scale(axis,value) = real(value-word($Settings[axis],2))/(word($Settings[axis],3)-word($Settings[axis],2))
    448 
    449 # Spider settings
    450 set style arrow 1 dt 1 lw 1.0 @fgal head filled size 0.06,25     # style for axes
    451 set style arrow 2 dt 2 lw 0.5 @fgal nohead   # style for weblines
    452 set style arrow 3 dt 1 lw 1 @fgal nohead     # style for axis tics
    453 set samples AxesCount
    454 set isosamples TicCount
    455 set urange[1:AxesCount]
    456 set vrange[1:TicCount]
    457 set style fill transparent solid 0.2
    458 
    459 set xrange[-Max-4*d:Max+4*d]
    460 set yrange[-Max-4*d:Max+4*d]
    461 plot \\
    462     '+' u (0):(0):(PosX($0,Max+d)):(PosY($0,Max+d)) w vec as 1 not, \\
    463     $Data u (LabX($0)): \\
    464 	(LabY($0)):1 every ::HeaderLines w labels center enhanced @fgt not, \\
    465     for [i=1:DataColCount] $Data u (PosX($0+1,Scale($0+1,column(i+1)))): \\
    466 	(PosY($0+1,Scale($0+1,column(i+1)))) every ::HeaderLines w filledcurves lt i title word($Data[1],i+1), \\
    467 %s
    468 #    '++' u (PosX($1,$2/TicCount)-TicdX($1,$2/TicCount)): \\
    469 #        (PosY($1,$2/TicCount)-TicdY($1,$2/TicCount)): \\
    470 #        (2*TicdX($1,$2/TicCount)):(2*TicdY($1,$2/TicCount)) \\
    471 #        w vec as 3 not, \\
    472 ### end of code
    473 ")
    474 
    475 (defvar org--plot/radar-ticks
    476   "    '++' u (PosX($1,$2/TicCount)):(PosY($1,$2/TicCount)): \\
    477 	(PosX($1+1,$2/TicCount)-PosX($1,$2/TicCount)):  \\
    478 	(PosY($1+1,$2/TicCount)-PosY($1,$2/TicCount)) w vec as 2 not, \\
    479     '++' u (TicLabelPosX(%s,$2)):(TicLabelPosY(%s,$2)): \\
    480 	(sprintf('%%g',TicValue(%s,$2))) w labels font ',8' @fgat not")
    481 
    482 (defvar org--plot/radar-setup-template
    483   "# Data
    484 $Data <<HEREHAVESOMEDATA
    485 %s
    486 HEREHAVESOMEDATA
    487 HeaderLines = 1
    488 
    489 # Settings for scale and offset adjustments
    490 # axis min max tics axisLabelXoff axisLabelYoff
    491 $Settings <<EOD
    492 %s
    493 EOD
    494 ")
    495 
    496 (defun org--plot/radar (table params)
    497   "Create gnuplot code for a radar plot of TABLE with PARAMS."
    498   (let* ((data
    499 	  (concat "\"" (mapconcat #'identity (plist-get params :labels) "\" \"") "\""
    500 		  "\n"
    501 		  (mapconcat (lambda (row)
    502 			       (format
    503 				"\"%s\" %s"
    504 				(car row)
    505 				(mapconcat #'identity (cdr row) " ")))
    506 			     (append table (list (car table)))
    507 			     "\n")))
    508 	 (ticks (or (plist-get params :ticks)
    509 		    (org--plot/sensible-tick-num table
    510 						 (plist-get params :ymin)
    511 						 (plist-get params :ymax))))
    512 	 (settings
    513 	  (mapconcat (lambda (row)
    514 		       (let ((data (org--plot/values-stats
    515 				    (mapcar #'string-to-number (cdr row)))))
    516 			 (format
    517 			  "\"%s\" %s %s %s"
    518 			  (car row)
    519 			  (or (plist-get params :ymin)
    520 			      (plist-get data :nice-min))
    521 			  (or (plist-get params :ymax)
    522 			      (plist-get data :nice-max))
    523 			  (if (eq ticks 0) 2 ticks)
    524 			  )))
    525 		     (append table (list (car table)))
    526 		     "\n"))
    527 	 (setup-file (make-temp-file "org-plot-setup")))
    528     (let ((coding-system-for-write 'utf-8))
    529       (write-region (format org--plot/radar-setup-template data settings) nil setup-file nil :silent))
    530     (format org--plot/radar-template
    531 	    setup-file
    532 	    (if (eq ticks 0) 2 ticks)
    533 	    (if (eq ticks 0) ""
    534 	      (apply #'format org--plot/radar-ticks
    535 		     (make-list 3 (if (and (plist-get params :ymin)
    536 					   (plist-get params :ymax))
    537 				      ;; FIXME multi-drawing of tick labels with "1"
    538 				      "1" "$1")))))))
    539 
    540 (defcustom org-plot/gnuplot-term-extra ""
    541   "String or function which provides the extra term options.
    542 E.g. a value of \"size 1050,650\" would cause
    543 \"set term ... size 1050,650\" to be used.
    544 If a function, it is called with the plot type as the argument."
    545   :group 'org-plot
    546   :type '(choice string function))
    547 
    548 (defun org-plot/gnuplot-script (table data-file num-cols params &optional preface)
    549   "Write a gnuplot script for TABLE to DATA-FILE respecting options in PARAMS.
    550 NUM-COLS controls the number of columns plotted in a 2-d plot.
    551 Optional argument PREFACE returns only option parameters in a
    552 manner suitable for prepending to a user-specified script."
    553   (let* ((type-name (plist-get params :plot-type))
    554 	 (type (cdr (assoc type-name org-plot/preset-plot-types))))
    555     (unless type
    556       (user-error "Org-plot type `%s' is undefined" type-name))
    557     (let* ((sets (plist-get params :set))
    558 	   (lines (plist-get params :line))
    559 	   (title (plist-get params :title))
    560 	   (file (plist-get params :file))
    561 	   (time-ind (plist-get params :timeind))
    562 	   (timefmt (plist-get params :timefmt))
    563 	   (x-labels (plist-get params :xlabels))
    564 	   (y-labels (plist-get params :ylabels))
    565 	   (plot-str (or (plist-get type :plot-str)
    566 			 "'%s' using %s%d%s with %s title '%s'"))
    567 	   (plot-cmd (plist-get type :plot-cmd))
    568 	   (plot-pre (plist-get type :plot-pre))
    569 	   (script "reset")
    570 	   ;; ats = add-to-script
    571 	   (ats (lambda (line) (when line (setf script (concat script "\n" line)))))
    572 	   plot-lines)
    573 
    574 
    575       ;; handle output file, background, and size
    576       (funcall ats (format "set term %s %s"
    577 			   (if file (file-name-extension file) "GNUTERM")
    578 			   (if (stringp org-plot/gnuplot-term-extra)
    579 			       org-plot/gnuplot-term-extra
    580 			     (funcall org-plot/gnuplot-term-extra type))))
    581       (when file ; output file
    582 	(funcall ats (format "set output '%s'" (expand-file-name file))))
    583 
    584       (when plot-pre
    585 	(funcall ats (funcall plot-pre table data-file num-cols params plot-str)))
    586 
    587       (funcall ats
    588 	       (if (stringp org-plot/gnuplot-script-preamble)
    589 		   org-plot/gnuplot-script-preamble
    590 		 (funcall org-plot/gnuplot-script-preamble type)))
    591 
    592       (when title (funcall ats (format "set title '%s'" title))) ; title
    593       (mapc ats lines)					       ; line
    594       (dolist (el sets) (funcall ats (format "set %s" el)))      ; set
    595       ;; Unless specified otherwise, values are TAB separated.
    596       (unless (string-match-p "^set datafile separator" script)
    597 	(funcall ats "set datafile separator \"\\t\""))
    598       (when x-labels			; x labels (xtics)
    599 	(funcall ats
    600 		 (format "set xtics (%s)"
    601 			 (mapconcat (lambda (pair)
    602 				      (format "\"%s\" %d" (cdr pair) (car pair)))
    603 				    x-labels ", "))))
    604       (when y-labels			; y labels (ytics)
    605 	(funcall ats
    606 		 (format "set ytics (%s)"
    607 			 (mapconcat (lambda (pair)
    608 				      (format "\"%s\" %d" (cdr pair) (car pair)))
    609 				    y-labels ", "))))
    610       (when time-ind			; timestamp index
    611 	(funcall ats "set xdata time")
    612 	(funcall ats (concat "set timefmt \""
    613 			     (or timefmt	; timefmt passed to gnuplot
    614 				 "%Y-%m-%d-%H:%M:%S") "\"")))
    615       (unless preface
    616 	(let ((type-func (plist-get type :plot-func)))
    617 	  (when type-func
    618 	    (setq plot-lines
    619 		  (funcall type-func table data-file num-cols params plot-str))))
    620 	(funcall ats
    621 		 (concat plot-cmd
    622 			 (when plot-cmd " ")
    623 			 (mapconcat #'identity
    624 				    (reverse plot-lines)
    625 				    ",\\\n    "))))
    626       script)))
    627 
    628 (defun org-plot/redisplay-img-in-buffer (img-file)
    629   "Find any overlays for IMG-FILE in the current Org buffer, and refresh them."
    630   (dolist (img-overlay org-inline-image-overlays)
    631     (when (string= img-file (plist-get (cdr (overlay-get img-overlay 'display)) :file))
    632       (when (and (file-exists-p img-file)
    633                  (fboundp 'image-flush))
    634         (image-flush (overlay-get img-overlay 'display))))))
    635 
    636 ;;-----------------------------------------------------------------------------
    637 ;; facade functions
    638 ;;;###autoload
    639 (defun org-plot/gnuplot (&optional params)
    640   "Plot table using gnuplot.  Gnuplot options can be specified with PARAMS.
    641 If not given options will be taken from the +PLOT
    642 line directly before or after the table."
    643   (interactive)
    644   (org-require-package 'gnuplot)
    645   (save-window-excursion
    646     ;; `gnuplot-send-buffer-to-gnuplot' will display *gnuplot* buffer
    647     ;; if `gnuplot-display-process' is non-nil.  Make it visible while
    648     ;; gnuplot is processing the data, preferably as a split, and
    649     ;; restore old window configuration after gnuplot finishes.
    650     (ignore-errors (delete-other-windows))
    651     (when (get-buffer "*gnuplot*") ; reset *gnuplot* if it already running
    652       (with-current-buffer "*gnuplot*"
    653 	(goto-char (point-max))))
    654     (save-excursion
    655       (org-plot/goto-nearest-table)
    656       ;; Set default options.
    657       (dolist (pair org-plot/gnuplot-default-options)
    658         (unless (plist-member params (car pair))
    659           (setf params (plist-put params (car pair) (cdr pair)))))
    660       ;; Collect options.
    661       (while (and (equal 0 (forward-line -1))
    662                   (looking-at "[[:space:]]*#\\+"))
    663         (setf params (org-plot/collect-options params))))
    664     ;; collect table and table information
    665     (let* ((data-file (make-temp-file "org-plot"))
    666            (table (let ((tbl (save-excursion
    667                                (org-plot/goto-nearest-table)
    668                                (org-table-to-lisp))))
    669 		    (when (pcase (plist-get params :transpose)
    670 			    (`y   t)
    671 			    (`yes t)
    672 			    (`t   t))
    673 		      (if (not (memq 'hline tbl))
    674 			  (setq tbl (apply #'cl-mapcar #'list tbl))
    675 			;; When present, remove hlines as they can't (currentily) be easily transposed.
    676 			(setq tbl (apply #'cl-mapcar #'list
    677 					 (remove 'hline tbl)))
    678 			(push 'hline (cdr tbl))))
    679 		    tbl))
    680 	   (num-cols (length (if (eq (nth 0 table) 'hline) (nth 1 table)
    681 			       (nth 0 table))))
    682 	   (type (assoc (plist-get params :plot-type)
    683 			org-plot/preset-plot-types))
    684            gnuplot-script)
    685 
    686       (unless type
    687 	(user-error "Org-plot type `%s' is undefined" (plist-get params :plot-type)))
    688 
    689       (run-with-idle-timer 0.1 nil #'delete-file data-file)
    690       (when (eq (cadr table) 'hline)
    691 	(setf params
    692 	      (plist-put params :labels (car table))) ; headers to labels
    693 	(setf table (delq 'hline (cdr table)))) ; clean non-data from table
    694       ;; Collect options.
    695       (save-excursion (while (and (equal 0 (forward-line -1))
    696 				  (looking-at "[[:space:]]*#\\+"))
    697 			(setf params (org-plot/collect-options params))))
    698       ;; Dump table to datafile
    699       (let ((dump-func (plist-get type :data-dump)))
    700         (if dump-func
    701 	    (funcall dump-func table data-file num-cols params)
    702 	  (org-plot/gnuplot-to-data table data-file params)))
    703       ;; Check type of ind column (timestamp? text?)
    704       (when (plist-get params :check-ind-type)
    705 	(let* ((ind (1- (plist-get params :ind)))
    706 	       (ind-column (mapcar (lambda (row) (nth ind row)) table)))
    707 	  (cond ((< ind 0) nil) ; ind is implicit
    708 		((cl-every (lambda (el)
    709 			     (string-match org-ts-regexp3 el))
    710 			   ind-column)
    711 		 (plist-put params :timeind t)) ; ind holds timestamps
    712 		((or (string= (plist-get params :with) "hist")
    713 		     (cl-notevery (lambda (el)
    714 				    (string-match org-table-number-regexp el))
    715 				  ind-column))
    716 		 (plist-put params :textind t))))) ; ind holds text
    717       ;; Write script.
    718       (setq gnuplot-script
    719             (org-plot/gnuplot-script
    720              table data-file num-cols params (plist-get params :script)))
    721       (with-temp-buffer
    722 	(if (plist-get params :script)	; user script
    723 	    (progn (insert gnuplot-script "\n")
    724 		   (insert-file-contents (plist-get params :script))
    725 		   (goto-char (point-min))
    726 		   (while (re-search-forward "\\$datafile" nil t)
    727 		     (replace-match data-file nil nil)))
    728 	  (insert gnuplot-script))
    729 	;; Graph table.
    730 	(gnuplot-mode)
    731         (condition-case nil
    732             (gnuplot-send-buffer-to-gnuplot)
    733           (buffer-read-only nil)))
    734       ;; Cleanup.
    735       (bury-buffer (get-buffer "*gnuplot*"))
    736       ;; Refresh any displayed images
    737       (when (plist-get params :file)
    738         (org-plot/redisplay-img-in-buffer (expand-file-name (plist-get params :file)))))))
    739 
    740 (provide 'org-plot)
    741 
    742 ;; Local variables:
    743 ;; generated-autoload-file: "org-loaddefs.el"
    744 ;; End:
    745 
    746 ;;; org-plot.el ends here