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