Commit | Line | Data |
---|---|---|
006793d0 CD |
1 | ;;; org-plot.el --- Support for plotting from Org-mode |
2 | ||
ba318903 | 3 | ;; Copyright (C) 2008-2014 Free Software Foundation, Inc. |
006793d0 CD |
4 | ;; |
5 | ;; Author: Eric Schulte <schulte dot eric at gmail dot com> | |
6 | ;; Keywords: tables, plotting | |
7 | ;; Homepage: http://orgmode.org | |
006793d0 CD |
8 | ;; |
9 | ;; This file is part of GNU Emacs. | |
10 | ;; | |
11 | ;; GNU Emacs is free software: you can redistribute it and/or modify | |
12 | ;; it under the terms of the GNU General Public License as published by | |
13 | ;; the Free Software Foundation, either version 3 of the License, or | |
14 | ;; (at your option) any later version. | |
15 | ||
16 | ;; GNU Emacs is distributed in the hope that it will be useful, | |
17 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of | |
18 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
19 | ;; GNU General Public License for more details. | |
20 | ||
21 | ;; You should have received a copy of the GNU General Public License | |
22 | ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. | |
23 | ||
24 | ;;; Commentary: | |
25 | ||
26 | ;; Borrows ideas and a couple of lines of code from org-exp.el. | |
27 | ||
28 | ;; Thanks to the org-mode mailing list for testing and implementation | |
29 | ;; and feature suggestions | |
30 | ||
31 | ;;; Code: | |
32 | (require 'org) | |
006793d0 | 33 | (require 'org-table) |
54a0dee5 | 34 | (eval-when-compile |
006793d0 CD |
35 | (require 'cl)) |
36 | ||
37 | (declare-function gnuplot-delchar-or-maybe-eof "ext:gnuplot" (arg)) | |
38 | (declare-function gnuplot-mode "ext:gnuplot" ()) | |
39 | (declare-function gnuplot-send-buffer-to-gnuplot "ext:gnuplot" ()) | |
40 | ||
41 | (defvar org-plot/gnuplot-default-options | |
42 | '((:plot-type . 2d) | |
43 | (:with . lines) | |
44 | (:ind . 0)) | |
86fbb8ca | 45 | "Default options to gnuplot used by `org-plot/gnuplot'.") |
006793d0 | 46 | |
93b62de8 CD |
47 | (defvar org-plot-timestamp-fmt nil) |
48 | ||
006793d0 CD |
49 | (defun org-plot/add-options-to-plist (p options) |
50 | "Parse an OPTIONS line and set values in the property list P. | |
51 | Returns the resulting property list." | |
52 | (let (o) | |
53 | (when options | |
93b62de8 CD |
54 | (let ((op '(("type" . :plot-type) |
55 | ("script" . :script) | |
56 | ("line" . :line) | |
57 | ("set" . :set) | |
58 | ("title" . :title) | |
59 | ("ind" . :ind) | |
60 | ("deps" . :deps) | |
61 | ("with" . :with) | |
62 | ("file" . :file) | |
63 | ("labels" . :labels) | |
64 | ("map" . :map) | |
c8d0cf5c | 65 | ("timeind" . :timeind) |
33306645 | 66 | ("timefmt" . :timefmt))) |
006793d0 CD |
67 | (multiples '("set" "line")) |
68 | (regexp ":\\([\"][^\"]+?[\"]\\|[(][^)]+?[)]\\|[^ \t\n\r;,.]*\\)") | |
69 | (start 0) | |
70 | o) | |
71 | (while (setq o (pop op)) | |
72 | (if (member (car o) multiples) ;; keys with multiple values | |
73 | (while (string-match | |
74 | (concat (regexp-quote (car o)) regexp) | |
75 | options start) | |
76 | (setq start (match-end 0)) | |
77 | (setq p (plist-put p (cdr o) | |
78 | (cons (car (read-from-string | |
79 | (match-string 1 options))) | |
80 | (plist-get p (cdr o))))) | |
81 | p) | |
82 | (if (string-match (concat (regexp-quote (car o)) regexp) | |
83 | options) | |
84 | (setq p (plist-put p (cdr o) | |
85 | (car (read-from-string | |
86 | (match-string 1 options))))))))))) | |
87 | p) | |
88 | ||
89 | (defun org-plot/goto-nearest-table () | |
90 | "Move the point forward to the beginning of nearest table. | |
91 | Return value is the point at the beginning of the table." | |
92 | (interactive) (move-beginning-of-line 1) | |
93 | (while (not (or (org-at-table-p) (< 0 (forward-line 1))))) | |
94 | (goto-char (org-table-begin))) | |
95 | ||
96 | (defun org-plot/collect-options (&optional params) | |
97 | "Collect options from an org-plot '#+Plot:' line. | |
98 | Accepts an optional property list PARAMS, to which the options | |
99 | will be added. Returns the resulting property list." | |
100 | (interactive) | |
101 | (let ((line (thing-at-point 'line))) | |
102 | (if (string-match "#\\+PLOT: +\\(.*\\)$" line) | |
103 | (org-plot/add-options-to-plist params (match-string 1 line)) | |
104 | params))) | |
105 | ||
93b62de8 CD |
106 | (defun org-plot-quote-timestamp-field (s) |
107 | "Convert field S from timestamp to Unix time and export to gnuplot." | |
108 | (format-time-string org-plot-timestamp-fmt (org-time-string-to-time s))) | |
109 | ||
006793d0 CD |
110 | (defun org-plot-quote-tsv-field (s) |
111 | "Quote field S for export to gnuplot." | |
112 | (if (string-match org-table-number-regexp s) s | |
93b62de8 | 113 | (if (string-match org-ts-regexp3 s) |
33306645 | 114 | (org-plot-quote-timestamp-field s) |
93b62de8 | 115 | (concat "\"" (mapconcat 'identity (split-string s "\"") "\"\"") "\"")))) |
006793d0 CD |
116 | |
117 | (defun org-plot/gnuplot-to-data (table data-file params) | |
118 | "Export TABLE to DATA-FILE in a format readable by gnuplot. | |
119 | Pass PARAMS through to `orgtbl-to-generic' when exporting TABLE." | |
120 | (with-temp-file | |
ff4be292 | 121 | data-file |
93b62de8 CD |
122 | (make-local-variable 'org-plot-timestamp-fmt) |
123 | (setq org-plot-timestamp-fmt (or | |
33306645 CD |
124 | (plist-get params :timefmt) |
125 | "%Y-%m-%d-%H:%M:%S")) | |
93b62de8 | 126 | (insert (orgtbl-to-generic |
33306645 CD |
127 | table |
128 | (org-combine-plists | |
129 | '(:sep "\t" :fmt org-plot-quote-tsv-field) | |
130 | params)))) | |
006793d0 CD |
131 | nil) |
132 | ||
133 | (defun org-plot/gnuplot-to-grid-data (table data-file params) | |
134 | "Export the data in TABLE to DATA-FILE for gnuplot. | |
ed21c5c8 | 135 | This means in a format appropriate for grid plotting by gnuplot. |
33306645 | 136 | PARAMS specifies which columns of TABLE should be plotted as independent |
e66ba1df | 137 | and dependant variables." |
006793d0 CD |
138 | (interactive) |
139 | (let* ((ind (- (plist-get params :ind) 1)) | |
140 | (deps (if (plist-member params :deps) | |
141 | (mapcar (lambda (val) (- val 1)) (plist-get params :deps)) | |
142 | (let (collector) | |
143 | (dotimes (col (length (first table))) | |
144 | (setf collector (cons col collector))) | |
145 | collector))) | |
8223b1d2 BG |
146 | (counter 0) |
147 | row-vals) | |
006793d0 CD |
148 | (when (>= ind 0) ;; collect values of ind col |
149 | (setf row-vals (mapcar (lambda (row) (setf counter (+ 1 counter)) | |
150 | (cons counter (nth ind row))) table))) | |
151 | (when (or deps (>= ind 0)) ;; remove non-plotting columns | |
152 | (setf deps (delq ind deps)) | |
153 | (setf table (mapcar (lambda (row) | |
154 | (dotimes (col (length row)) | |
155 | (unless (memq col deps) | |
156 | (setf (nth col row) nil))) | |
157 | (delq nil row)) | |
158 | table))) | |
159 | ;; write table to gnuplot grid datafile format | |
160 | (with-temp-file data-file | |
161 | (let ((num-rows (length table)) (num-cols (length (first table))) | |
8223b1d2 BG |
162 | (gnuplot-row (lambda (col row value) |
163 | (setf col (+ 1 col)) (setf row (+ 1 row)) | |
164 | (format "%f %f %f\n%f %f %f\n" | |
165 | col (- row 0.5) value ;; lower edge | |
166 | col (+ row 0.5) value))) ;; upper edge | |
006793d0 | 167 | front-edge back-edge) |
8223b1d2 BG |
168 | (dotimes (col num-cols) |
169 | (dotimes (row num-rows) | |
170 | (setf back-edge | |
171 | (concat back-edge | |
172 | (funcall gnuplot-row (- col 1) row | |
173 | (string-to-number (nth col (nth row table)))))) | |
174 | (setf front-edge | |
175 | (concat front-edge | |
176 | (funcall gnuplot-row col row | |
177 | (string-to-number (nth col (nth row table))))))) | |
178 | ;; only insert once per row | |
179 | (insert back-edge) (insert "\n") ;; back edge | |
180 | (insert front-edge) (insert "\n") ;; front edge | |
181 | (setf back-edge "") (setf front-edge "")))) | |
006793d0 CD |
182 | row-vals)) |
183 | ||
c8d0cf5c | 184 | (defun org-plot/gnuplot-script (data-file num-cols params &optional preface) |
006793d0 | 185 | "Write a gnuplot script to DATA-FILE respecting the options set in PARAMS. |
c8d0cf5c CD |
186 | NUM-COLS controls the number of columns plotted in a 2-d plot. |
187 | Optional argument PREFACE returns only option parameters in a | |
188 | manner suitable for prepending to a user-specified script." | |
006793d0 CD |
189 | (let* ((type (plist-get params :plot-type)) |
190 | (with (if (equal type 'grid) | |
191 | 'pm3d | |
192 | (plist-get params :with))) | |
193 | (sets (plist-get params :set)) | |
194 | (lines (plist-get params :line)) | |
195 | (map (plist-get params :map)) | |
196 | (title (plist-get params :title)) | |
197 | (file (plist-get params :file)) | |
198 | (ind (plist-get params :ind)) | |
33306645 CD |
199 | (time-ind (plist-get params :timeind)) |
200 | (timefmt (plist-get params :timefmt)) | |
006793d0 CD |
201 | (text-ind (plist-get params :textind)) |
202 | (deps (if (plist-member params :deps) (plist-get params :deps))) | |
203 | (col-labels (plist-get params :labels)) | |
204 | (x-labels (plist-get params :xlabels)) | |
205 | (y-labels (plist-get params :ylabels)) | |
206 | (plot-str "'%s' using %s%d%s with %s title '%s'") | |
207 | (plot-cmd (case type | |
3ab2c837 BG |
208 | ('2d "plot") |
209 | ('3d "splot") | |
210 | ('grid "splot"))) | |
8223b1d2 BG |
211 | (script "reset") |
212 | ; ats = add-to-script | |
213 | (ats (lambda (line) (setf script (format "%s\n%s" script line)))) | |
214 | plot-lines) | |
215 | (when file ;; output file | |
216 | (funcall ats (format "set term %s" (file-name-extension file))) | |
217 | (funcall ats (format "set output '%s'" file))) | |
218 | (case type ;; type | |
219 | ('2d ()) | |
220 | ('3d (if map (funcall ats "set map"))) | |
221 | ('grid (if map (funcall ats "set pm3d map") | |
222 | (funcall ats "set pm3d")))) | |
223 | (when title (funcall ats (format "set title '%s'" title))) ;; title | |
224 | (when lines (mapc (lambda (el) (funcall ats el)) lines)) ;; line | |
225 | (when sets ;; set | |
226 | (mapc (lambda (el) (funcall ats (format "set %s" el))) sets)) | |
227 | (when x-labels ;; x labels (xtics) | |
228 | (funcall ats | |
229 | (format "set xtics (%s)" | |
230 | (mapconcat (lambda (pair) | |
231 | (format "\"%s\" %d" (cdr pair) (car pair))) | |
232 | x-labels ", ")))) | |
233 | (when y-labels ;; y labels (ytics) | |
234 | (funcall ats | |
235 | (format "set ytics (%s)" | |
236 | (mapconcat (lambda (pair) | |
237 | (format "\"%s\" %d" (cdr pair) (car pair))) | |
238 | y-labels ", ")))) | |
239 | (when time-ind ;; timestamp index | |
240 | (funcall ats "set xdata time") | |
241 | (funcall ats (concat "set timefmt \"" | |
242 | (or timefmt ;; timefmt passed to gnuplot | |
243 | "%Y-%m-%d-%H:%M:%S") "\""))) | |
244 | (unless preface | |
245 | (case type ;; plot command | |
3ab2c837 | 246 | ('2d (dotimes (col num-cols) |
006793d0 CD |
247 | (unless (and (equal type '2d) |
248 | (or (and ind (equal (+ 1 col) ind)) | |
249 | (and deps (not (member (+ 1 col) deps))))) | |
250 | (setf plot-lines | |
251 | (cons | |
252 | (format plot-str data-file | |
ed21c5c8 CD |
253 | (or (and ind (> ind 0) |
254 | (not text-ind) | |
255 | (format "%d:" ind)) "") | |
006793d0 CD |
256 | (+ 1 col) |
257 | (if text-ind (format ":xticlabel(%d)" ind) "") | |
258 | with | |
259 | (or (nth col col-labels) (format "%d" (+ 1 col)))) | |
260 | plot-lines))))) | |
3ab2c837 | 261 | ('3d |
006793d0 CD |
262 | (setq plot-lines (list (format "'%s' matrix with %s title ''" |
263 | data-file with)))) | |
3ab2c837 | 264 | ('grid |
006793d0 CD |
265 | (setq plot-lines (list (format "'%s' with %s title ''" |
266 | data-file with))))) | |
8223b1d2 BG |
267 | (funcall ats |
268 | (concat plot-cmd " " (mapconcat 'identity (reverse plot-lines) ",\\\n ")))) | |
269 | script)) | |
006793d0 CD |
270 | |
271 | ;;----------------------------------------------------------------------------- | |
272 | ;; facade functions | |
273 | ;;;###autoload | |
274 | (defun org-plot/gnuplot (&optional params) | |
86fbb8ca | 275 | "Plot table using gnuplot. Gnuplot options can be specified with PARAMS. |
006793d0 CD |
276 | If not given options will be taken from the +PLOT |
277 | line directly before or after the table." | |
278 | (interactive) | |
279 | (require 'gnuplot) | |
280 | (save-window-excursion | |
281 | (delete-other-windows) | |
282 | (when (get-buffer "*gnuplot*") ;; reset *gnuplot* if it already running | |
81ad75af | 283 | (with-current-buffer "*gnuplot*" |
8bfe682a | 284 | (goto-char (point-max)) |
006793d0 CD |
285 | (gnuplot-delchar-or-maybe-eof nil))) |
286 | (org-plot/goto-nearest-table) | |
287 | ;; set default options | |
288 | (mapc | |
289 | (lambda (pair) | |
290 | (unless (plist-member params (car pair)) | |
291 | (setf params (plist-put params (car pair) (cdr pair))))) | |
292 | org-plot/gnuplot-default-options) | |
293 | ;; collect table and table information | |
294 | (let* ((data-file (make-temp-file "org-plot")) | |
295 | (table (org-table-to-lisp)) | |
296 | (num-cols (length (if (eq (first table) 'hline) (second table) | |
297 | (first table))))) | |
298 | (while (equal 'hline (first table)) (setf table (cdr table))) | |
299 | (when (equal (second table) 'hline) | |
300 | (setf params (plist-put params :labels (first table))) ;; headers to labels | |
301 | (setf table (delq 'hline (cdr table)))) ;; clean non-data from table | |
302 | ;; collect options | |
303 | (save-excursion (while (and (equal 0 (forward-line -1)) | |
ed21c5c8 | 304 | (looking-at "[[:space:]]*#\\+")) |
006793d0 CD |
305 | (setf params (org-plot/collect-options params)))) |
306 | ;; dump table to datafile (very different for grid) | |
307 | (case (plist-get params :plot-type) | |
3ab2c837 BG |
308 | ('2d (org-plot/gnuplot-to-data table data-file params)) |
309 | ('3d (org-plot/gnuplot-to-data table data-file params)) | |
310 | ('grid (let ((y-labels (org-plot/gnuplot-to-grid-data | |
006793d0 CD |
311 | table data-file params))) |
312 | (when y-labels (plist-put params :ylabels y-labels))))) | |
93b62de8 | 313 | ;; check for timestamp ind column |
006793d0 | 314 | (let ((ind (- (plist-get params :ind) 1))) |
33306645 CD |
315 | (when (and (>= ind 0) (equal '2d (plist-get params :plot-type))) |
316 | (if (= (length | |
317 | (delq 0 (mapcar | |
006793d0 | 318 | (lambda (el) |
93b62de8 | 319 | (if (string-match org-ts-regexp3 el) |
006793d0 CD |
320 | 0 1)) |
321 | (mapcar (lambda (row) (nth ind row)) table)))) 0) | |
93b62de8 | 322 | (plist-put params :timeind t) |
33306645 | 323 | ;; check for text ind column |
0bd48b37 CD |
324 | (if (or (string= (plist-get params :with) "hist") |
325 | (> (length | |
326 | (delq 0 (mapcar | |
327 | (lambda (el) | |
328 | (if (string-match org-table-number-regexp el) | |
329 | 0 1)) | |
330 | (mapcar (lambda (row) (nth ind row)) table)))) 0)) | |
33306645 | 331 | (plist-put params :textind t))))) |
006793d0 CD |
332 | ;; write script |
333 | (with-temp-buffer | |
334 | (if (plist-get params :script) ;; user script | |
c8d0cf5c CD |
335 | (progn (insert |
336 | (org-plot/gnuplot-script data-file num-cols params t)) | |
337 | (insert "\n") | |
338 | (insert-file-contents (plist-get params :script)) | |
339 | (goto-char (point-min)) | |
340 | (while (re-search-forward "$datafile" nil t) | |
341 | (replace-match data-file nil nil))) | |
006793d0 CD |
342 | (insert |
343 | (org-plot/gnuplot-script data-file num-cols params))) | |
344 | ;; graph table | |
345 | (gnuplot-mode) | |
346 | (gnuplot-send-buffer-to-gnuplot)) | |
347 | ;; cleanup | |
93b62de8 | 348 | (bury-buffer (get-buffer "*gnuplot*")) |
c8d0cf5c | 349 | (run-with-idle-timer 0.1 nil (lambda () (delete-file data-file)))))) |
006793d0 CD |
350 | |
351 | (provide 'org-plot) | |
352 | ||
bdebdb64 BG |
353 | ;; Local variables: |
354 | ;; generated-autoload-file: "org-loaddefs.el" | |
355 | ;; End: | |
356 | ||
006793d0 | 357 | ;;; org-plot.el ends here |