1 ;;; eplot.el --- Manage and Edit Wordpress Posts -*- lexical-binding: t -*-
3 ;; Copyright (C) 2024 Free Software Foundation, Inc.
5 ;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
9 ;; Package-Requires: ((emacs "29.0.59") (pcsv "0.0"))
11 ;; eplot is free software; you can redistribute it and/or modify it
12 ;; under the terms of the GNU General Public License as published by
13 ;; the Free Software Foundation; either version 2, or (at your option)
18 ;; The main entry point is `M-x eplot' in a buffer with time series
21 ;; If installing manually, put something like the following in your
22 ;; Emacs init file (but adjust the path to where you've put eplot):
24 ;; (push "~/src/eplot/" load-path)
25 ;; (autoload 'eplot "eplot" nil t)
26 ;; (autoload 'eplot-mode "eplot" nil t)
27 ;; (unless (assoc "\\.plt" auto-mode-alist)
28 ;; (setq auto-mode-alist (cons '("\\.plt" . eplot-mode) auto-mode-alist)))
30 ;; This requires the pcsv package to parse CSV files.
41 (defvar eplot--user-defaults nil)
42 (defvar eplot--chart-headers nil)
43 (defvar eplot--plot-headers nil)
44 (defvar eplot--transient-settings nil)
48 '("aliceblue" "antiquewhite" "aqua" "aquamarine" "azure" "beige" "bisque"
49 "black" "blanchedalmond" "blue" "blueviolet" "brown" "burlywood"
50 "cadetblue" "chartreuse" "chocolate" "coral" "cornflowerblue" "cornsilk"
51 "crimson" "cyan" "darkblue" "darkcyan" "darkgoldenrod" "darkgray"
52 "darkgreen" "darkgrey" "darkkhaki" "darkmagenta" "darkolivegreen"
53 "darkorange" "darkorchid" "darkred" "darksalmon" "darkseagreen"
54 "darkslateblue" "darkslategray" "darkslategrey" "darkturquoise"
55 "darkviolet" "deeppink" "deepskyblue" "dimgray" "dimgrey" "dodgerblue"
56 "firebrick" "floralwhite" "forestgreen" "fuchsia" "gainsboro" "ghostwhite"
57 "gold" "goldenrod" "gray" "green" "greenyellow" "grey" "honeydew" "hotpink"
58 "indianred" "indigo" "ivory" "khaki" "lavender" "lavenderblush" "lawngreen"
59 "lemonchiffon" "lightblue" "lightcoral" "lightcyan" "lightgoldenrodyellow"
60 "lightgray" "lightgreen" "lightgrey" "lightpink" "lightsalmon"
61 "lightseagreen" "lightskyblue" "lightslategray" "lightslategrey"
62 "lightsteelblue" "lightyellow" "lime" "limegreen" "linen" "magenta"
63 "maroon" "mediumaquamarine" "mediumblue" "mediumorchid" "mediumpurple"
64 "mediumseagreen" "mediumslateblue" "mediumspringgreen" "mediumturquoise"
65 "mediumvioletred" "midnightblue" "mintcream" "mistyrose" "moccasin"
66 "navajowhite" "navy" "oldlace" "olive" "olivedrab" "orange" "orangered"
67 "orchid" "palegoldenrod" "palegreen" "paleturquoise" "palevioletred"
68 "papayawhip" "peachpuff" "peru" "pink" "plum" "powderblue" "purple" "red"
69 "rosybrown" "royalblue" "saddlebrown" "salmon" "sandybrown" "seagreen"
70 "seashell" "sienna" "silver" "skyblue" "slateblue" "slategray" "slategrey"
71 "snow" "springgreen" "steelblue" "tan" "teal" "thistle" "tomato"
72 "turquoise" "violet" "wheat" "white" "whitesmoke" "yellow" "yellowgreen"))
74 (defun eplot-set (header value)
75 "Set the default value of HEADER to VALUE.
76 To get a list of all possible HEADERs, use the `M-x
77 eplot-list-chart-headers' command.
79 Also see `eplot-reset'."
80 (let ((elem (or (assq header eplot--chart-headers)
81 (assq header eplot--plot-headers))))
83 (error "No such header type: %s" header))
84 (eplot--add-default header value)))
86 (defun eplot--add-default (header value)
87 ;; We want to preserve the order defaults have been added, so that
88 ;; we can apply them in the same order. This makes a difference
89 ;; when we're dealing with specs that have inheritence.
90 (setq eplot--user-defaults (delq (assq header eplot--user-defaults)
91 eplot--user-defaults))
92 (setq eplot--user-defaults (list (cons header value))))
94 (defun eplot-reset (&optional header)
95 "Reset HEADER to defaults.
96 If HEADER is nil or not present, reset everything to defaults."
98 (setq eplot--user-defaults (delq (assq header eplot--user-defaults)
99 eplot--user-defaults))
100 (setq eplot--user-defaults nil)))
102 (unless (assoc "\\.plt" auto-mode-alist)
103 (setq auto-mode-alist (cons '("\\.plt" . eplot-mode) auto-mode-alist)))
107 (defvar-keymap eplot-mode-map
108 "C-c C-c" #'eplot-update-view-buffer
109 "C-c C-p" #'eplot-switch-view-buffer
110 "C-c C-e" #'eplot-list-chart-headers
111 "C-c C-v" #'eplot-customize
112 "C-c C-l" #'eplot-create-controls
113 "TAB" #'eplot-complete)
115 ;; # is working overtime in the syntax here:
116 ;; It can be a color like Color: #e0e0e0, and
117 ;; it can be a setting like 33 # Label: Apples,
118 ;; when it starts a line it's a comment.
119 (defvar eplot-font-lock-keywords
120 `(("^[ \t\n]*#.*" . font-lock-comment-face)
121 ("^[^ :\n]+:" . font-lock-keyword-face)
122 ("#[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]\\([0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]\\)?" . font-lock-variable-name-face)
123 ("#.*" . font-lock-builtin-face)))
125 (define-derived-mode eplot-mode text-mode "eplot"
126 "Major mode for editing charts.
127 Use the \\[eplot-list-chart-headers] command to get a list of all
128 possible chart headers."
129 (setq-local completion-at-point-functions
130 (cons 'eplot--complete-header completion-at-point-functions))
131 (setq-local font-lock-defaults
132 '(eplot-font-lock-keywords nil nil nil)))
134 (defun eplot-complete ()
138 ((let ((completion-fail-discreetly t))
139 (completion-at-point))
140 ;; Completion was performed; nothing else to do.
142 (t (indent-relative))))
144 (defun eplot--complete-header ()
146 ;; Complete headers names.
147 (and (or (looking-at ".*:")
148 (and (looking-at "[ \t]*$")
151 (not (looking-at "\\(.+\\):")))))
153 (let ((headers (mapcar
155 (if (looking-at ".*:")
156 (capitalize (symbol-name (car h)))
157 (concat (capitalize (symbol-name (car h))) ": ")))
159 ;; If we're after the headers, then we want
160 ;; to complete over the plot headers. Otherwise,
161 ;; complete over the chart headers.
162 (if (and (not (bobp))
165 (re-search-backward "^[ \t]*$" nil t)))
167 eplot--chart-headers))))
168 (completion-ignore-case t))
169 (completion-in-region (pos-bol) (line-end-position) headers)
170 'completion-attempted)))
171 ;; Complete header values.
174 (and (looking-at "[ \t]*$")
177 (and (looking-at "\\(.+\\):")
178 (setq hname (intern (downcase (match-string 1)))))))
180 (let ((valid (plist-get
181 (cdr (assq hname (append eplot--plot-headers
182 eplot--chart-headers)))
184 (completion-ignore-case t))
185 (completion-in-region
187 (search-backward ":" (pos-bol) t)
188 (skip-chars-forward ": \t")
191 (mapcar #'symbol-name valid))
192 'completion-attempted)))))))
194 (define-minor-mode eplot-minor-mode
195 "Minor mode to issue commands from an eplot data buffer."
198 (defvar-keymap eplot-minor-mode-map
199 "H-l" #'eplot-eval-and-update)
201 (defvar-keymap eplot-view-mode-map
202 "s" #'eplot-view-write-file
203 "w" #'eplot-view-write-scaled-file
204 "c" #'eplot-view-customize
205 "l" #'eplot-create-controls)
207 (define-derived-mode eplot-view-mode special-mode "eplot view"
208 "Major mode for displaying eplots."
209 (setq-local revert-buffer-function #'eplot-update
212 (defun eplot-view-write-file (file &optional width)
213 "Write the current chart to a file.
214 If you type in a file name that ends with something else than \"svg\",
215 ImageMagick \"convert\" will be used to convert the image first.
217 If writing to a PNG file, \"rsvg-conver\" will be used instead if
218 it exists as this usually gives better results."
219 (interactive "FWrite to file name: ")
220 (when (and (file-exists-p file)
221 (not (yes-or-no-p "File exists, overwrite? ")))
222 (error "Not overwriting the file"))
224 (goto-char (point-min))
226 (text-property-search-forward 'display nil
229 (eq (car e) 'image))))))
231 (error "Can't find an image in the current buffer"))
232 (let ((svg (plist-get (cdr (prop-match-value match)) :data))
233 (tmp " *eplot convert*")
234 (executable (if width "rsvg-convert" "convert"))
237 (error "Invalid image in the current buffer"))
239 (set-buffer-multibyte nil)
241 (if (string-match-p "\\.svg\\'" file)
242 (write-region (point-min) (point-max) file)
243 (if (and (string-match-p "\\.png\\'" file)
244 (executable-find "rsvg-convert"))
245 (setq executable "rsvg-convert")
246 (unless (executable-find executable)
247 (error "%s isn't installed; can only save svg files"
249 (when (and (equal executable "rsvg-convert")
250 (not (string-match-p "\\.png\\'" file))
251 (not (executable-find "convert")))
252 (error "Can only write PNG files when scaling because \"convert\" isn't installed"))
255 (setq sfile (make-temp-file "eplot" nil ".svg")
256 ofile (make-temp-file "eplot" nil ".png"))
257 (write-region (point-min) (point-max) sfile nil 'silent)
258 ;; We don't use `call-process-region', because
259 ;; convert doesn't seem to like that?
260 (let ((code (if (equal executable "rsvg-convert")
263 executable nil (get-buffer-create tmp) nil
264 `(,(format "--output=%s"
265 (expand-file-name ofile))
267 `(,(format "--width=%d" width)
268 "--keep-aspect-ratio"))
271 executable nil (get-buffer-create tmp) nil
273 (eplot--view-error code tmp)
274 (when (file-exists-p ofile)
275 (if (string-match-p "\\.png\\'" file)
276 (rename-file ofile file)
277 (let ((code (call-process "convert" nil tmp nil
279 (eplot--view-error code tmp))))
280 (message "Wrote %s" file)))
282 (when (get-buffer tmp)
284 (when (file-exists-p sfile)
286 (when (file-exists-p ofile)
287 (delete-file sfile)))))))))
289 (defun eplot--view-error (code tmp)
291 (error "Error code %d: %s"
293 (with-current-buffer tmp
294 (while (search-forward "[ \t\n]+" nil t)
296 (string-trim (buffer-string))))))
298 (defun eplot-view-write-scaled-file (width file)
299 "Write the current chart to a rescaled to a file.
300 The rescaling is done by \"rsvg-convert\", which has to be
301 installed. Rescaling is done when rendering, so this should give
302 you a clear, non-blurry version of the chart at any size."
303 (interactive "nWidth: \nFWrite to file: ")
304 (eplot-view-write-file file width))
306 (defun eplot-view-customize ()
307 "Customize the settings for the chart in the current buffer."
309 (with-suppressed-warnings ((interactive-only eplot-customize))
312 (defvar eplot--data-buffer nil)
313 (defvar eplot--current-chart nil)
316 "Plot the data in the current buffer."
318 (eplot-update-view-buffer))
320 (defun eplot-with-headers (header-file)
321 "Plot the data in the current buffer using headers from a file."
322 (interactive "fHeader file: ")
323 (eplot-update-view-buffer
325 (insert-file-contents header-file)
326 (eplot--parse-headers))))
328 (defun eplot-switch-view-buffer ()
329 "Switch to the eplot view buffer and render the chart."
331 (eplot-update-view-buffer nil t))
333 (defun eplot-update-view-buffer (&optional headers switch)
334 "Update the eplot view buffer based on the current data buffer."
336 ;; This is mainly useful during implementation.
337 (if (and (eq major-mode 'emacs-lisp-mode)
338 (get-buffer-window "*eplot*" t))
339 (with-current-buffer "*eplot*"
341 (when-let ((win (get-buffer-window "*eplot*" t)))
342 (set-window-point win (point-min))))
344 (let* ((eplot--user-defaults (eplot--settings-table))
345 (data (eplot--parse-buffer))
346 (data-buffer (current-buffer))
347 (window (selected-window)))
349 (user-error "No data in the current buffer"))
350 (setq data (eplot--inject-headers data headers))
351 (if (get-buffer-window "*eplot*" t)
352 (set-buffer "*eplot*")
354 (pop-to-buffer-same-window "*eplot*")
355 (pop-to-buffer "*eplot*")))
356 (let ((inhibit-read-only t))
358 (unless (eq major-mode 'eplot-view-mode)
360 (setq-local eplot--data-buffer data-buffer)
361 (let ((chart (eplot--render data)))
362 (with-current-buffer data-buffer
363 (setq-local eplot--current-chart chart)))
365 (when-let ((win (get-buffer-window "*eplot*" t)))
366 (set-window-point win (point-min))))
367 (select-window window))))
369 (defun eplot--settings-table ()
370 (if (not eplot--transient-settings)
372 (append eplot--user-defaults eplot--transient-settings)))
374 (defun eplot--inject-headers (data headers)
375 ;; It's OK not to separate the plot headers from the chart
376 ;; headers. Collect them here, if any.
377 (when-let ((plot-headers
378 (cl-loop for elem in (mapcar #'car eplot--plot-headers)
379 for value = (eplot--vs elem headers)
382 ;; Remove these headers from the data
383 ;; headers so that we don't get errors
384 ;; on undefined headers.
385 (setq headers (delq (assq elem headers)
387 (cons elem value)))))
388 (dolist (plot (cdr (assq :plots data)))
389 (let ((headers (assq :headers plot)))
391 (nconc headers plot-headers)
392 (nconc plot (list (list :headers plot-headers)))))))
393 (append data headers))
395 (defun eplot-eval-and-update ()
396 "Helper command when developing."
397 (interactive nil emacs-lisp-mode)
398 (save-some-buffers t)
399 (elisp-eval-region-or-buffer)
401 (eplot-update-view-buffer))
405 (defun eplot-update (&rest _ignore)
406 "Update the plot in the current buffer."
408 (unless eplot--data-buffer
409 (user-error "No data buffer associated with this eplot view buffer"))
410 (let ((data (with-current-buffer eplot--data-buffer
411 (eplot--parse-buffer)))
412 (eplot--user-defaults (with-current-buffer eplot--data-buffer
413 (eplot--settings-table)))
414 (inhibit-read-only t))
416 (let ((chart (eplot--render data)))
417 (with-current-buffer eplot--data-buffer
418 (setq-local eplot--current-chart chart)))
421 (defun eplot--parse-buffer ()
422 (if (eq major-mode 'org-mode)
423 (eplot--parse-org-buffer)
424 (eplot--parse-eplot-buffer)))
426 (defun eplot--parse-eplot-buffer ()
427 (if (eplot--csv-buffer-p)
428 (eplot--parse-csv-buffer)
429 (let ((buf (current-buffer)))
431 (insert-buffer-substring buf)
432 ;; Remove comments first.
433 (goto-char (point-min))
434 (while (re-search-forward "^[ \t]*#" nil t)
436 (goto-char (point-min))
438 (let* ((data (eplot--parse-headers))
440 ;; It's OK not to separate the plot headers from the chart
441 ;; headers. Collect them here, if any.
442 (cl-loop for elem in (mapcar #'car eplot--plot-headers)
443 for value = (eplot--vs elem data)
446 ;; Remove these headers from the data
447 ;; headers so that we don't get errors
448 ;; on undefined headers.
449 (setq data (delq (assq elem data) data))
453 (while-let ((plot (eplot--parse-values nil plot-headers)))
454 (setq plot-headers nil)
457 (push (cons :plots (nreverse plots)) data))
460 (defun eplot--parse-headers ()
463 (while (looking-at "\\([^\n\t :]+\\):\\(.*\\)")
464 (setq type (intern (downcase (match-string 1)))
465 value (substring-no-properties (string-trim (match-string 2))))
467 ;; Get continuation lines.
468 (while (looking-at "[ \t]+\\(.*\\)")
469 (setq value (concat value " " (string-trim (match-string 1))))
471 (if (eq type 'header-file)
472 (setq data (nconc data
474 (insert-file-contents value)
475 (eplot--parse-headers))))
476 ;; We don't use `push' here because we want to preserve order
477 ;; also when inserting headers from other files.
478 (setq data (nconc data (list (cons type value))))))
481 (defun eplot--parse-values (&optional in-headers data-headers)
482 ;; Skip past separator lines.
483 (while (looking-at "[ \t]*\n")
486 ;; We may have plot-specific headers.
487 (headers (nconc (eplot--parse-headers) data-headers))
488 (data-format (or (eplot--vyl 'data-format headers)
489 (eplot--vyl 'data-format in-headers)))
490 (two-values (memq 'two-values data-format))
491 (xy (or (memq 'year data-format)
492 (memq 'date data-format)
493 (memq 'time data-format)
494 (memq 'xy data-format)))
495 (data-column (or (eplot--vn 'data-column headers)
496 (eplot--vn 'data-column in-headers))))
497 (if-let ((data-file (eplot--vs 'data-file headers)))
499 (insert-file-contents data-file)
500 (setq values (cdr (assq :values (eplot--parse-values headers)))
501 headers (delq (assq 'data headers) headers)))
502 ;; Now we come to the data. The data is typically either just a
503 ;; number, or two numbers (in which case the first number is a
504 ;; date or a time). Labels ans settings can be introduced with
506 (while (looking-at "\\([-0-9. \t]+\\)\\([ \t]*#\\(.*\\)\\)?")
507 (let ((numbers (match-string 1))
508 (settings (eplot--parse-settings (match-string 3)))
510 (setq numbers (mapcar #'string-to-number
511 (split-string (string-trim numbers))))
512 ;; If we're reading two dimensionalish data, the first
513 ;; number is the date/time/x.
515 (setq this (list :x (pop numbers))))
516 ;; Chop off all the numbers until we read the column(s)
519 (setq numbers (nthcdr (1- data-column) numbers)))
521 (setq this (nconc this (list :value (pop numbers)))))
523 (setq this (nconc this (list :extra-value (pop numbers)))))
525 (setq this (nconc this (list :settings settings))))
526 (when (plist-get this :value)
529 (setq values (nreverse values)))
531 `((:headers . ,headers) (:values . ,values)))))
533 (defun eplot--parse-settings (string)
536 (insert (string-trim string) "\n")
537 (goto-char (point-min))
538 (while (re-search-forward "\\(.\\)," nil t)
539 (if (equal (match-string 1) "\\")
540 (replace-match "," t t)
543 (when (looking-at "[ \t]+")
544 (replace-match ""))))
545 (goto-char (point-min))
546 (eplot--parse-headers))))
550 (defun eplot--vn (type data &optional default)
551 (if-let ((value (cdr (assq type data))))
552 (string-to-number value)
555 (defun eplot--vs (type data &optional default)
556 (or (cdr (assq type data)) default))
558 (defun eplot--vy (type data &optional default)
559 (if-let ((value (cdr (assq type data))))
560 (intern (downcase value))
563 (defun eplot--vyl (type data &optional default)
564 (if-let ((value (cdr (assq type data))))
565 (mapcar #'intern (split-string (downcase value)))
568 (defmacro eplot-def (args doc-string)
569 (declare (indent defun))
570 `(eplot--def ',(nth 0 args) ',(nth 1 args) ',(nth 2 args) ',(nth 3 args)
573 (defun eplot--def (name type default valid doc)
574 (setq eplot--chart-headers (delq (assq name eplot--chart-headers)
575 eplot--chart-headers))
581 eplot--chart-headers))
583 (eplot-def (width number)
584 "The width of the entire chart.")
586 (eplot-def (height number)
587 "The height of the entire chart.")
589 (eplot-def (format symbol normal (normal bar-chart horizontal-bar-chart))
590 "The overall format of the chart.")
592 (eplot-def (layout symbol nil (normal compact))
593 "The general layout of the chart.")
595 (eplot-def (mode symbol light (dark light))
598 (eplot-def (margin-left number 70)
601 (eplot-def (margin-right number 20)
604 (eplot-def (margin-top number 40)
607 (eplot-def (margin-bottom number 60)
608 "The bottom margin.")
610 (eplot-def (x-axis-title-space number 5)
611 "The space between the X axis and the label.")
613 (eplot-def (font string "sans-serif")
614 "The font to use in titles, labels and legends.")
616 (eplot-def (font-size number 12)
619 (eplot-def (font-weight symbol bold (bold normal))
622 (eplot-def (label-font string (spec font))
623 "The font to use for axes labels.")
625 (eplot-def (label-font-size number (spec font-size))
626 "The font size to use for axes labels.")
628 (eplot-def (bar-font string (spec font))
629 "The font to use for bar chart labels.")
631 (eplot-def (bar-font-size number (spec font-size))
632 "The font size to use for bar chart labels.")
634 (eplot-def (bar-font-weight symbol (spec font-weight) (bold normal))
635 "The font weight to use for bar chart labels.")
637 (eplot-def (chart-color string "black")
638 "The foreground color to use in plots, axes, legends, etc.
639 This is used as the default, but can be overridden per thing.")
641 (eplot-def (background-color string "white")
642 "The background color.
643 If you want a chart with a transparent background, use the color
646 (eplot-def (background-gradient string)
647 "Use this to get a gradient color in the background.")
649 (eplot-def (axes-color string (spec chart-color))
650 "The color of the axes.")
652 (eplot-def (grid-color string "#e0e0e0")
653 "The color of the grid.")
655 (eplot-def (grid symbol xy (xy x y off))
656 "What grid axes to do.")
658 (eplot-def (grid-opacity number)
659 "The opacity of the grid.
660 This should either be nil or a value between 0 and 1, where 0 is
663 (eplot-def (grid-position symbol bottom (bottom top))
664 "Whether to put the grid on top or under the plot.")
666 (eplot-def (legend symbol nil (true nil))
667 "Whether to do a legend.")
669 (eplot-def (legend-color string (spec chart-color))
670 "The color of legends (if any).")
672 (eplot-def (legend-border-color string (spec chart-color))
673 "The border color of legends (if any).")
675 (eplot-def (legend-background-color string (spec background-color))
676 "The background color of legends (if any).")
678 (eplot-def (label-color string (spec axes-color))
679 "The color of labels on the axes.")
681 (eplot-def (surround-color string)
682 "The color between the plot area and the edges of the chart.")
684 (eplot-def (border-color string)
685 "The color of the border of the chart, if any.")
687 (eplot-def (border-width number)
688 "The width of the border of the chart, if any.")
690 (eplot-def (frame-color string)
691 "The color of the frame of the plot, if any.")
693 (eplot-def (frame-width number)
694 "The width of the frame of the plot, if any.")
696 (eplot-def (min number)
697 "The minimum value in the chart.
698 This is normally computed automatically, but can be overridden
701 (eplot-def (max number)
702 "The maximum value in the chart.
703 This is normally computed automatically, but can be overridden
706 (eplot-def (title string)
707 "The title of the chart, if any.")
709 (eplot-def (title-color string (spec chart-color))
710 "The color of the title.")
712 (eplot-def (x-title string)
713 "The title of the X axis, if any.")
715 (eplot-def (y-title string)
716 "The title of the X axis, if any.")
718 (eplot-def (x-label-format string)
719 "Format string for the X labels.
720 This is a `format' string.")
722 (eplot-def (y-label-format string)
723 "Format string for the Y labels.
724 This is a `format' string.")
726 (eplot-def (x-label-orientation symbol horizontal (horizontal vertical))
727 "Orientation of the X labels.")
729 (eplot-def (background-image-file string)
730 "Use an image as the background.")
732 (eplot-def (background-image-opacity number 1)
733 "The opacity of the background image.")
735 (eplot-def (background-image-cover symbol all (all plot frame))
736 "Position of the background image.
737 Valid values are `all' (the entire image), `plot' (the plot area)
738 and `frame' (the surrounding area).")
740 (eplot-def (header-file string)
741 "File where the headers are.")
743 (defvar eplot-compact-defaults
749 (x-axis-title-space 3)))
751 (defvar eplot-dark-defaults
752 '((chart-color "#c0c0c0")
753 (axes-color "#c0c0c0")
754 (grid-color "#404040")
755 (background-color "#101010")
756 (label-color "#c0c0c0")
757 (legend-color "#c0c0c0")
758 (title-color "#c0c0c0")))
760 (defvar eplot-bar-chart-defaults
761 '((grid-position top)
766 (defvar eplot-horizontal-bar-chart-defaults
767 '((grid-position top)
771 (defclass eplot-chart ()
773 (plots :initarg :plots)
774 (data :initarg :data)
777 (x-values :initform nil)
778 (x-type :initform nil)
788 (x-step-map :initform nil)
791 (inhibit-compute-x-step :initform nil)
792 ;; ---- CUT HERE ----
793 (axes-color :initarg :axes-color :initform nil)
794 (background-color :initarg :background-color :initform nil)
795 (background-gradient :initarg :background-gradient :initform nil)
796 (background-image-cover :initarg :background-image-cover :initform nil)
797 (background-image-file :initarg :background-image-file :initform nil)
798 (background-image-opacity :initarg :background-image-opacity :initform nil)
799 (bar-font :initarg :bar-font :initform nil)
800 (bar-font-size :initarg :bar-font-size :initform nil)
801 (bar-font-weight :initarg :bar-font-weight :initform nil)
802 (border-color :initarg :border-color :initform nil)
803 (border-width :initarg :border-width :initform nil)
804 (chart-color :initarg :chart-color :initform nil)
805 (font :initarg :font :initform nil)
806 (font-size :initarg :font-size :initform nil)
807 (font-weight :initarg :font-weight :initform nil)
808 (format :initarg :format :initform nil)
809 (frame-color :initarg :frame-color :initform nil)
810 (frame-width :initarg :frame-width :initform nil)
811 (grid :initarg :grid :initform nil)
812 (grid-color :initarg :grid-color :initform nil)
813 (grid-opacity :initarg :grid-opacity :initform nil)
814 (grid-position :initarg :grid-position :initform nil)
815 (header-file :initarg :header-file :initform nil)
816 (height :initarg :height :initform nil)
817 (label-color :initarg :label-color :initform nil)
818 (label-font :initarg :label-font :initform nil)
819 (label-font-size :initarg :label-font-size :initform nil)
820 (layout :initarg :layout :initform nil)
821 (legend :initarg :legend :initform nil)
822 (legend-background-color :initarg :legend-background-color :initform nil)
823 (legend-border-color :initarg :legend-border-color :initform nil)
824 (legend-color :initarg :legend-color :initform nil)
825 (margin-bottom :initarg :margin-bottom :initform nil)
826 (margin-left :initarg :margin-left :initform nil)
827 (margin-right :initarg :margin-right :initform nil)
828 (margin-top :initarg :margin-top :initform nil)
829 (max :initarg :max :initform nil)
830 (min :initarg :min :initform nil)
831 (mode :initarg :mode :initform nil)
832 (surround-color :initarg :surround-color :initform nil)
833 (title :initarg :title :initform nil)
834 (title-color :initarg :title-color :initform nil)
835 (width :initarg :width :initform nil)
836 (x-axis-title-space :initarg :x-axis-title-space :initform nil)
837 (x-title :initarg :x-title :initform nil)
838 (y-title :initarg :y-title :initform nil)
839 (x-label-format :initarg :x-label-format :initform nil)
840 (x-label-orientation :initarg :x-label-orientation :initform nil)
841 (y-label-format :initarg :y-label-format :initform nil)
842 ;; ---- CUT HERE ----
845 ;;; Parameters that are plot specific.
847 (defmacro eplot-pdef (args doc-string)
848 (declare (indent defun))
849 `(eplot--pdef ',(nth 0 args) ',(nth 1 args) ',(nth 2 args) ',(nth 3 args)
852 (defun eplot--pdef (name type default valid doc)
853 (setq eplot--plot-headers (delq (assq name eplot--plot-headers)
854 eplot--plot-headers))
860 eplot--plot-headers))
862 (eplot-pdef (smoothing symbol nil (moving-average nil))
863 "Smoothing algorithm to apply to the data, if any.
864 Valid values are `moving-average' and, er, probably more to come.")
866 (eplot-pdef (gradient string)
867 "Gradient to apply to the plot.
870 from-color to-color direction position
872 The last two parameters are optional.
874 direction is either `top-down' (the default), `bottom-up',
875 `left-right' or `right-left').
877 position is either `below' or `above'.
879 to-color can be either a color name, or a string that defines
882 Gradient: black 25-purple-50-white-75-purple-black
884 In that case, the second element specifies the percentage points
885 of where each color ends, so the above starts with black, then at
886 25% it's purple, then at 50% it's white, then it's back to purple
887 again at 75%, before ending up at black at a 100% (but you don't
888 have to include the 100% here -- it's understood).")
890 (eplot-pdef (style symbol line ( line impulse point square circle cross
891 triangle rectangle curve))
892 "Style the plot should be drawn in.
893 Valid values are listed below. Some styles take additional
897 Straight lines between values.
900 Curved lines between values.
903 size: width of the impulse
910 size: diameter of the circle
911 fill-color: color to fill the center
914 size: length of the lines in the cross
917 size: length of the sides of the triangle
918 fill-color: color to fill the center
921 size: length of the sides of the rectangle
922 fill-color: color to fill the center")
924 (eplot-pdef (fill-color string)
925 "Color to use to fill the plot styles that are closed shapes.
926 I.e., circle, triangle and rectangle.")
928 (eplot-pdef (color string (spec chart-color))
929 "Color to draw the plot.")
931 (eplot-pdef (data-format symbol single (single date time xy))
933 By default, eplot assumes that each line has a single data point.
934 This can also be `date', `time' and `xy'.
936 date: The first column is a date on ISO8601 format (i.e., YYYYMMDD).
938 time: The first column is a clock (i.e., HHMMSS).
940 xy: The first column is the X position.")
942 (eplot-pdef (data-column number 1)
943 "Column where the data is.")
945 (eplot-pdef (fill-border-color string)
946 "Border around the fill area when using a fill/gradient style.")
948 (eplot-pdef (size number)
949 "Size of elements in styles that have meaningful sizes.")
951 (eplot-pdef (size-factor number)
952 "Multiply the size of the elements by the value.")
954 (eplot-pdef (data-file string)
955 "File where the data is.")
957 (eplot-pdef (data-format symbol-list nil (nil two-values date time))
958 "List of symbols to describe the data format.
959 Elements allowed are `two-values', `date' and `time'.")
961 (eplot-pdef (name string)
962 "Name of the plot, which will be displayed if legends are switched on.")
964 (eplot-pdef (legend-color string (spec chart-color))
965 "Color for the name to be displayed in the legend.")
967 (eplot-pdef (bezier-factor number 0.1)
968 "The Bezier factor to apply to curve plots.")
970 (defclass eplot-plot ()
972 (values :initarg :values)
973 ;; ---- CUT HERE ----
974 (bezier-factor :initarg :bezier-factor :initform nil)
975 (color :initarg :color :initform nil)
976 (data-column :initarg :data-column :initform nil)
977 (data-file :initarg :data-file :initform nil)
978 (data-format :initarg :data-format :initform nil)
979 (fill-border-color :initarg :fill-border-color :initform nil)
980 (fill-color :initarg :fill-color :initform nil)
981 (gradient :initarg :gradient :initform nil)
982 (legend-color :initarg :legend-color :initform nil)
983 (name :initarg :name :initform nil)
984 (size :initarg :size :initform nil)
985 (size-factor :initarg :size-factor :initform nil)
986 (smoothing :initarg :smoothing :initform nil)
987 (style :initarg :style :initform nil)
988 ;; ---- CUT HERE ----
991 (defun eplot--make-plot (data)
992 "Make an `eplot-plot' object and initialize based on DATA."
993 (let ((plot (make-instance 'eplot-plot
994 :values (cdr (assq :values data)))))
995 ;; Get the program-defined defaults.
996 (eplot--object-defaults plot eplot--plot-headers)
997 ;; One special case. I don't think this hack is quite right...
998 (when (or (eq (eplot--vs 'mode data) 'dark)
999 (eq (cdr (assq 'mode eplot--user-defaults)) 'dark))
1000 (setf (slot-value plot 'color) "#c0c0c0"))
1002 (eplot--object-values plot (cdr (assq :headers data)) eplot--plot-headers)
1005 (defun eplot--make-chart (data)
1006 "Make an `eplot-chart' object and initialize based on DATA."
1007 (let ((chart (make-instance 'eplot-chart
1008 :plots (mapcar #'eplot--make-plot
1009 (eplot--vs :plots data))
1011 ;; First get the program-defined defaults.
1012 (eplot--object-defaults chart eplot--chart-headers)
1013 ;; Then do the "meta" variables.
1014 (eplot--meta chart data 'mode 'dark eplot-dark-defaults)
1015 (eplot--meta chart data 'layout 'compact eplot-compact-defaults)
1016 (eplot--meta chart data 'format 'bar-chart eplot-bar-chart-defaults)
1017 (eplot--meta chart data 'format 'horizontal-bar-chart
1018 eplot-horizontal-bar-chart-defaults)
1019 ;; Set defaults from user settings/transients.
1020 (cl-loop for (name . value) in eplot--user-defaults
1021 when (assq name eplot--chart-headers)
1023 (setf (slot-value chart name) value)
1024 (eplot--set-dependent-values chart name value))
1025 ;; Finally, use the data from the chart.
1026 (eplot--object-values chart data eplot--chart-headers)
1027 ;; If not set, recompute the margins based on the font sizes (if
1028 ;; the font size has been changed from defaults).
1029 (when (or (assq 'font-size eplot--user-defaults)
1030 (assq 'font-size data))
1031 (with-slots ( title x-title y-title
1032 margin-top margin-bottom margin-left
1033 font-size font font-weight)
1035 (when (or title x-title y-title)
1037 (eplot--text-height (concat title x-title y-title)
1038 font font-size font-weight)))
1040 (and (not (assq 'margin-top eplot--user-defaults))
1041 (not (assq 'margin-top data))))
1042 (cl-incf margin-top (* text-height 1.4)))
1044 (and (not (assq 'margin-bottom eplot--user-defaults))
1045 (not (assq 'margin-bottom data))))
1046 (cl-incf margin-bottom (* text-height 1.4)))
1048 (and (not (assq 'margin-left eplot--user-defaults))
1049 (not (assq 'margin-left data))))
1050 (cl-incf margin-left (* text-height 1.4)))))))
1053 (defun eplot--meta (chart data slot value defaults)
1054 (when (or (eq (cdr (assq slot eplot--user-defaults)) value)
1055 (eq (eplot--vy slot data) value))
1056 (eplot--set-theme chart defaults)))
1058 (defun eplot--object-defaults (object headers)
1059 (dolist (header headers)
1060 (when-let ((default (plist-get (cdr header) :default)))
1061 (setf (slot-value object (car header))
1062 ;; Allow overrides via `eplot-set'.
1063 (or (cdr (assq (car header) eplot--user-defaults))
1064 (if (and (consp default)
1065 (eq (car default) 'spec))
1066 ;; Chase dependencies.
1067 (eplot--default (cadr default))
1070 (defun eplot--object-values (object data headers)
1071 (cl-loop for (name . value) in data
1072 do (unless (eq name :plots)
1073 (let ((spec (cdr (assq name headers))))
1075 (error "%s is not a valid spec" name)
1077 (cl-case (plist-get spec :type)
1079 (string-to-number value))
1081 (intern (downcase value)))
1083 (mapcar #'intern (split-string (downcase value))))
1086 (setf (slot-value object name) value)
1087 (eplot--set-dependent-values object name value)))))))
1089 (defun eplot--set-dependent-values (object name value)
1090 (dolist (slot (gethash name (eplot--dependecy-graph)))
1091 (setf (slot-value object slot) value)
1092 (eplot--set-dependent-values object slot value)))
1094 (defun eplot--set-theme (chart map)
1095 (cl-loop for (slot value) in map
1096 do (setf (slot-value chart slot) value)))
1098 (defun eplot--default (slot)
1099 "Find the default value for SLOT, chasing dependencies."
1100 (let ((spec (cdr (assq slot eplot--chart-headers))))
1102 (error "Invalid slot %s" slot))
1103 (let ((default (plist-get spec :default)))
1104 (if (and (consp default)
1105 (eq (car default) 'spec))
1106 (eplot--default (cadr default))
1107 (or (cdr (assq slot eplot--user-defaults)) default)))))
1109 (defun eplot--dependecy-graph ()
1110 (let ((table (make-hash-table)))
1111 (dolist (elem eplot--chart-headers)
1112 (let ((default (plist-get (cdr elem) :default)))
1113 (when (and (consp default)
1114 (eq (car default) 'spec))
1115 (push (car elem) (gethash (cadr default) table)))))
1118 (defun eplot--render (data &optional return-image)
1119 "Create the chart and display it.
1120 If RETURN-IMAGE is non-nil, return it instead of displaying it."
1121 (let* ((chart (eplot--make-chart data))
1123 (with-slots ( width height xs ys
1124 margin-left margin-right margin-top margin-bottom
1125 grid-position plots x-min format
1126 x-label-orientation)
1128 ;; Set the size of the chart based on the window it's going to
1129 ;; be displayed in. It uses the *eplot* window by default, or
1130 ;; the current one if that isn't displayed.
1131 (let ((factor (image-compute-scaling-factor image-scaling-factor)))
1133 (setq width (truncate
1134 (/ (* (window-pixel-width
1135 (get-buffer-window "*eplot*" t))
1139 (setq height (truncate
1140 (/ (* (window-pixel-height
1141 (get-buffer-window "*eplot*" t))
1144 (setq svg (svg-create width height)
1145 xs (- width margin-left margin-right)
1146 ys (- height margin-top margin-bottom))
1147 ;; Protect against being called in an empty buffer.
1149 ;; Sanity check against the user choosing dimensions
1150 ;; that leave no space for the plot.
1152 ;; Just draw the basics.
1153 (eplot--draw-basics svg chart)
1155 ;; Horizontal bar charts are special.
1156 (when (eq format 'horizontal-bar-chart)
1157 (eplot--adjust-horizontal-bar-chart chart data))
1158 ;; Compute min/max based on all plots, and also compute x-ticks
1160 (eplot--compute-chart-dimensions chart)
1161 (when (and (eq x-label-orientation 'vertical)
1162 (eplot--default-p 'margin-bottom (slot-value chart 'data)))
1163 (eplot--adjust-vertical-x-labels chart))
1164 ;; Analyze values and adjust values accordingly.
1165 (eplot--adjust-chart chart)
1166 ;; Compute the Y labels -- this may adjust `margin-left'.
1167 (eplot--compute-y-labels chart)
1168 ;; Compute the X labels -- this may adjust `margin-bottom'.
1169 (eplot--compute-x-labels chart)
1170 ;; Draw background/borders/titles/etc.
1171 (eplot--draw-basics svg chart)
1173 (when (eq grid-position 'top)
1174 (eplot--draw-plots svg chart))
1176 (eplot--draw-x-ticks svg chart)
1177 (unless (eq format 'horizontal-bar-chart)
1178 (eplot--draw-y-ticks svg chart))
1181 (with-slots ( margin-left margin-right margin-margin-top
1182 margin-bottom axes-color)
1184 (svg-line svg margin-left margin-top margin-left
1185 (+ (- height margin-bottom) 5)
1187 (svg-line svg (- margin-left 5) (- height margin-bottom)
1188 (- width margin-right) (- height margin-bottom)
1189 :stroke axes-color))
1191 (when (eq grid-position 'bottom)
1192 (eplot--draw-plots svg chart)))
1194 (with-slots (frame-color frame-width) chart
1195 (when (or frame-color frame-width)
1196 (svg-rectangle svg margin-left margin-top xs ys
1197 :stroke-width frame-width
1199 :stroke-color frame-color)))
1200 (eplot--draw-legend svg chart))
1204 (svg-insert-image svg)
1207 (defun eplot--adjust-horizontal-bar-chart (chart data)
1208 (with-slots ( plots bar-font bar-font-size bar-font-weight margin-left
1209 width margin-right xs)
1211 (with-slots ( data-format values) (car plots)
1212 (push 'xy data-format)
1213 ;; Flip the values -- we want the values to be on the X
1216 (cl-loop for value in values
1218 collect (list :value i
1219 :x (plist-get value :value)
1221 (plist-get value :settings))))
1222 (when (eplot--default-p 'margin-left data)
1224 (+ (cl-loop for value in values
1227 (eplot--vs 'label (plist-get value :settings))
1228 bar-font bar-font-size bar-font-weight))
1230 xs (- width margin-left margin-right))))))
1232 (defun eplot--draw-basics (svg chart)
1233 (with-slots ( width height
1234 chart-color font font-size font-weight
1235 margin-left margin-right margin-top margin-bottom
1236 background-color label-color
1240 (eplot--draw-background chart svg 0 0 width height)
1241 (with-slots ( background-image-file background-image-opacity
1242 background-image-cover)
1244 (when (and background-image-file
1245 ;; Sanity checks to avoid erroring out later.
1246 (file-exists-p background-image-file)
1247 (file-regular-p background-image-file))
1248 (apply #'svg-embed svg background-image-file "image/jpeg" nil
1249 :opacity background-image-opacity
1250 :preserveAspectRatio "xMidYMid slice"
1251 (if (memq background-image-cover '(all frame))
1252 `(:x 0 :y 0 :width ,width :height ,height)
1253 `(:x ,margin-left :y ,margin-top :width ,xs :height ,ys)))
1254 (when (eq background-image-cover 'frame)
1255 (eplot--draw-background chart svg margin-left margin-right xs ys))))
1256 ;; Area between plot and edges.
1257 (with-slots (surround-color) chart
1258 (when surround-color
1259 (svg-rectangle svg 0 0 width height
1260 :fill surround-color)
1261 (svg-rectangle svg margin-left margin-top
1263 :fill background-color)))
1264 ;; Border around the entire chart.
1265 (with-slots (border-width border-color) chart
1266 (when (or border-width border-color)
1267 (svg-rectangle svg 0 0 width height
1268 :stroke-width (or border-width 1)
1270 :stroke-color (or border-color chart-color))))
1271 ;; Frame around the plot.
1272 (with-slots (frame-width frame-color) chart
1273 (when (or frame-width frame-color)
1274 (svg-rectangle svg margin-left margin-top xs ys
1275 :stroke-width (or frame-width 1)
1277 :stroke-color (or frame-color chart-color))))
1278 ;; Title and legends.
1279 (with-slots (title title-color) chart
1283 :text-anchor "middle"
1284 :font-weight font-weight
1285 :font-size font-size
1287 :x (+ margin-left (/ (- width margin-left margin-right) 2))
1288 :y (+ 3 (/ margin-top 2)))))
1289 (with-slots (x-title) chart
1291 (svg-text svg x-title
1293 :text-anchor "middle"
1294 :font-weight font-weight
1295 :font-size font-size
1297 :x (+ margin-left (/ (- width margin-left margin-right) 2))
1298 :y (- height (/ margin-bottom 4)))))
1299 (with-slots (y-title) chart
1302 (eplot--text-height y-title font font-size font-weight)))
1303 (svg-text svg y-title
1305 :text-anchor "middle"
1306 :font-weight font-weight
1307 :font-size font-size
1310 (format "translate(%s,%s) rotate(-90)"
1311 (- (/ margin-left 2) (/ text-height 2) 4)
1313 (/ (- height margin-bottom margin-top) 2)))))))))
1315 (defun eplot--draw-background (chart svg left top width height)
1316 (with-slots (background-gradient background-color) chart
1317 (let ((gradient (eplot--parse-gradient background-gradient))
1320 (setq id (format "gradient-%s" (make-temp-name "grad")))
1321 (eplot--gradient svg id 'linear
1322 (eplot--stops (eplot--vs 'from gradient)
1323 (eplot--vs 'to gradient))
1324 (eplot--vs 'direction gradient)))
1325 (apply #'svg-rectangle svg left top width height
1328 `(:fill ,background-color))))))
1330 (defun eplot--compute-chart-dimensions (chart)
1331 (with-slots ( min max plots x-values x-min x-max x-ticks
1332 print-format font-size
1334 inhibit-compute-x-step x-type x-step-map format
1335 x-tick-step x-label-step
1336 label-font label-font-size x-label-format)
1340 (dolist (plot plots)
1341 (with-slots (values data-format) plot
1342 (let* ((vals (nconc (seq-map (lambda (v) (plist-get v :value)) values)
1343 (and (memq 'two-values data-format)
1345 (lambda (v) (plist-get v :extra-value))
1347 ;; Set the x-values based on the first plot.
1349 (setq print-format (cond
1350 ((memq 'year data-format) 'year)
1351 ((memq 'date data-format) 'date)
1352 ((memq 'time data-format) 'time)
1355 ((or (memq 'xy data-format)
1356 (memq 'year data-format))
1357 (setq x-values (cl-loop for val in values
1358 collect (plist-get val :x))
1359 x-min (if (eq format 'horizontal-bar-chart)
1362 x-max (seq-max x-values)
1363 x-ticks (eplot--get-ticks x-min x-max xs))
1364 (when (memq 'year data-format)
1365 (setq print-format 'literal-year)))
1366 ((memq 'date data-format)
1368 (cl-loop for val in values
1372 (decoded-time-set-defaults
1374 (format "%d" (plist-get val :x)))))))
1375 x-min (seq-min x-values)
1376 x-max (seq-max x-values)
1377 inhibit-compute-x-step t)
1378 (let ((xs (eplot--get-date-ticks
1380 label-font label-font-size x-label-format)))
1381 (setq x-ticks (car xs)
1382 print-format (cadr xs)
1385 x-step-map (nth 2 xs))))
1386 ((memq 'time data-format)
1388 (cl-loop for val in values
1392 (decoded-time-set-defaults
1394 (format "%06d" (plist-get val :x)))))
1396 x-min (car x-values)
1397 x-max (car (last x-values))
1398 inhibit-compute-x-step t)
1399 (let ((xs (eplot--get-time-ticks
1400 x-min x-max xs label-font label-font-size
1402 (setq x-ticks (car xs)
1403 print-format (cadr xs)
1406 x-step-map (nth 2 xs))))
1408 ;; This is a one-dimensional plot -- we don't have X
1409 ;; values, really, so we just do zero to (1- (length
1411 (setq x-type 'one-dimensional
1412 x-values (cl-loop for i from 0
1413 repeat (length values)
1415 x-min (car x-values)
1416 x-max (car (last x-values))
1417 x-ticks x-values))))
1419 (setq min (min (or min 1.0e+INF) (seq-min vals))))
1421 (setq max (max (or max -1.0e+INF) (seq-max vals))))))))))
1423 (defun eplot--adjust-chart (chart)
1424 (with-slots ( x-tick-step x-label-step y-tick-step y-label-step
1425 min max ys format inhibit-compute-x-step
1426 y-ticks xs x-values print-format
1427 x-label-format label-font label-font-size data
1430 (setq y-ticks (and max
1433 ;; We get 5% more ticks to check whether we
1434 ;; should extend max.
1435 (if (eplot--default-p 'max data)
1439 (when (eplot--default-p 'max data)
1440 (setq max (max max (car (last y-ticks)))))
1441 (if (eq format 'bar-chart)
1444 (unless inhibit-compute-x-step
1445 (let ((xt (eplot--compute-x-ticks
1446 xs x-ticks print-format
1447 x-label-format label-font label-font-size)))
1448 (setq x-tick-step (car xt)
1449 x-label-step (cadr xt)))))
1451 (let ((yt (eplot--compute-y-ticks
1453 (eplot--text-height "100" label-font label-font-size))))
1454 (setq y-tick-step (car yt)
1455 y-label-step (cadr yt))))
1456 ;; If max is less than 2% off from a pleasant number, then
1458 (when (eplot--default-p 'max data)
1459 (cl-loop for tick in (reverse y-ticks)
1460 when (and (< max tick)
1461 (< (e/ (- tick max) (- max min)) 0.02))
1464 ;; Chop off any further ticks.
1465 (setcdr (member tick y-ticks) nil))))
1468 (if (and (eplot--default-p 'min data)
1469 (< (car y-ticks) min))
1470 (setq min (car y-ticks))
1471 ;; We may be extending the bottom of the chart to get pleasing
1472 ;; numbers. We don't want to be drawing the chart on top of the
1473 ;; X axis, because the chart won't be visible there.
1475 (<= min (car y-ticks))
1476 ;; But not if we start at origo, because that just
1479 (setq min (- (car y-ticks)
1480 ;; 2% of the value range.
1481 (* 0.02 (- (car (last y-ticks)) (car y-ticks))))))))))
1483 (defun eplot--adjust-vertical-x-labels (chart)
1484 (with-slots ( x-step-map x-ticks format plots
1485 print-format x-label-format label-font
1486 label-font-size margin-bottom
1487 bar-font bar-font-size bar-font-weight)
1492 for xv in (or x-step-map x-ticks)
1493 for x = (if (consp xv) (car xv) xv)
1495 for value = (and (equal format 'bar-chart)
1496 (elt (slot-value (car plots) 'values) i))
1497 for label = (if (equal format 'bar-chart)
1499 (plist-get value :settings)
1500 ;; When we're doing bar charts, we
1501 ;; want default labeling to start with
1503 (format "%s" (1+ x)))
1504 (eplot--format-value x print-format x-label-format))
1505 maximize (if (equal format 'bar-chart)
1507 label bar-font bar-font-size bar-font-weight)
1509 label label-font label-font-size)))))
1510 ;; Ensure that we have enough room to display the X labels
1511 ;; (unless overridden).
1512 (with-slots ( height margin-top ys
1513 y-ticks y-tick-step y-label-step min max)
1515 (setq margin-bottom (max margin-bottom (+ width 40))
1516 ys (- height margin-top margin-bottom))))))
1518 (defun eplot--compute-x-labels (chart)
1519 (with-slots ( x-step-map x-ticks
1520 format plots print-format x-label-format x-labels
1521 x-tick-step x-label-step
1522 x-label-orientation margin-bottom)
1527 for xv in (or x-step-map x-ticks)
1528 for x = (if (consp xv) (car xv) xv)
1529 for do-tick = (if (consp xv)
1531 (zerop (e% x x-tick-step)))
1532 for do-label = (if (consp xv)
1534 (zerop (e% x x-label-step)))
1536 for value = (and (equal format 'bar-chart)
1537 (elt (slot-value (car plots) 'values) i))
1539 (if (equal format 'bar-chart)
1541 (plist-get value :settings)
1542 ;; When we're doing bar charts, we
1543 ;; want default labeling to start with
1545 (format "%s" (1+ x)))
1546 (eplot--format-value x print-format x-label-format))
1550 (defun eplot--draw-x-ticks (svg chart)
1551 (with-slots ( x-step-map x-ticks format layout print-format
1552 margin-left margin-right margin-top margin-bottom
1555 axes-color label-color
1556 grid grid-opacity grid-color
1557 font x-tick-step x-label-step x-label-format x-label-orientation
1558 label-font label-font-size
1560 bar-font bar-font-size bar-font-weight)
1562 (let ((font label-font)
1563 (font-size label-font-size)
1564 (font-weight 'normal))
1565 (when (equal format 'bar-chart)
1567 font-size bar-font-size
1568 font-weight bar-font-weight))
1570 (cl-loop with label-height
1571 for xv in (or x-step-map x-ticks)
1572 for x = (if (consp xv) (car xv) xv)
1574 for (label do-tick do-label) in x-labels
1575 for stride = (eplot--stride chart x-ticks)
1576 for px = (if (equal format 'bar-chart)
1577 (+ margin-left (* x stride) (/ stride 2)
1578 (/ (* stride 0.1) 2))
1580 (* (/ (- (* 1.0 x) x-min) (- x-max x-min))
1582 ;; We might have one extra stride outside the area -- don't
1584 when (<= px (- width margin-right))
1587 ;; Draw little tick.
1588 (unless (equal format 'bar-chart)
1590 px (- height margin-bottom)
1591 px (+ (- height margin-bottom)
1595 :stroke axes-color))
1596 (when (or (eq grid 'xy) (eq grid 'x))
1597 (svg-line svg px margin-top
1598 px (- height margin-bottom)
1599 :opacity grid-opacity
1600 :stroke grid-color)))
1602 ;; We want to skip marking the first X value
1603 ;; unless we're a bar chart or we're a one
1604 ;; dimensional chart.
1605 (or (equal format 'bar-chart)
1607 (not (= x-min (car x-values)))
1608 (eq x-type 'one-dimensional)
1609 (and (not (zerop x)) (not (zerop i)))))
1610 (if (eq x-label-orientation 'vertical)
1612 (unless label-height
1613 ;; The X position we're putting the label at is
1614 ;; based on the bottom of the lower-case
1615 ;; characters. So we want to ignore descenders
1616 ;; etc, so we use "xx" to determine the height
1617 ;; to be able to center the text.
1620 ;; If the labels are numerical, we need
1621 ;; to center them using the height of
1623 (if (string-match "^[0-9]+$" label)
1625 ;; Otherwise center them on the baseline.
1627 font font-size font-weight)))
1631 :font-size font-size
1632 :font-weight font-weight
1635 (format "translate(%s,%s) rotate(-90)"
1636 (+ px (/ label-height 2))
1637 (- height margin-bottom -10))))
1640 :text-anchor "middle"
1641 :font-size font-size
1642 :font-weight font-weight
1645 :y (+ (- height margin-bottom)
1647 (if (equal format 'bar-chart)
1648 (if (equal layout 'compact) 3 5)
1651 (defun eplot--stride (chart values)
1652 (with-slots (xs x-type format) chart
1653 (if (eq x-type 'one-dimensional)
1655 ;; Fenceposting bar-chart vs everything else.
1656 (if (eq format 'bar-chart)
1658 (1- (length values))))
1659 (e/ xs (length values)))))
1661 (defun eplot--default-p (slot data)
1662 "Return non-nil if SLOT is at the default value."
1663 (and (not (assq slot eplot--user-defaults))
1664 (not (assq slot data))))
1666 (defun eplot--compute-y-labels (chart)
1667 (with-slots ( y-ticks y-labels
1668 width height min max xs ys
1669 margin-top margin-bottom margin-left margin-right
1670 y-tick-step y-label-step y-label-format)
1672 ;; First collect all the labels we're thinking about outputting.
1674 (cl-loop for y in y-ticks
1675 for py = (- (- height margin-bottom)
1676 (* (/ (- (* 1.0 y) min) (- max min))
1678 when (and (<= margin-top py (- height margin-bottom))
1679 (zerop (e% y y-tick-step))
1680 (zerop (e% y y-label-step)))
1681 collect (eplot--format-y
1682 y (- (cadr y-ticks) (car y-ticks)) nil
1684 ;; Check the labels to see whether we have too many digits for
1685 ;; what we're actually going to display. Man, this is a lot of
1686 ;; back-and-forth and should be rewritten to be less insanely
1688 (when (= (seq-count (lambda (label)
1689 (string-match "\\." label))
1693 (cl-loop with max = (cl-loop for label in y-labels
1694 maximize (eplot--decimal-digits
1695 (string-to-number label)))
1696 for label in y-labels
1697 collect (format (if (zerop max)
1699 (format "%%.%df" max))
1700 (string-to-number label)))))
1701 (setq y-labels (cl-coerce y-labels 'vector))
1702 ;; Ensure that we have enough room to display the Y labels
1703 ;; (unless overridden).
1704 (when (eplot--default-p 'margin-left (slot-value chart 'data))
1705 (with-slots (label-font label-font-size) chart
1706 (setq margin-left (max margin-left
1707 (+ (eplot--text-width
1708 (elt y-labels (1- (length y-labels)))
1709 label-font label-font-size)
1711 xs (- width margin-left margin-right))))))
1713 (defun eplot--draw-y-ticks (svg chart)
1714 (with-slots ( y-ticks y-labels y-tick-step y-label-step label-color
1715 label-font label-font-size
1716 width height min max ys
1717 margin-top margin-bottom margin-left margin-right
1719 grid grid-opacity grid-color)
1722 (cl-loop with lnum = 0
1723 with text-height = (eplot--text-height
1724 "012" label-font label-font-size)
1727 for py = (- (- height margin-bottom)
1728 (* (/ (- (* 1.0 y) min) (- max min))
1731 (when (and (<= margin-top py (- height margin-bottom))
1732 (zerop (e% y y-tick-step)))
1733 (svg-line svg margin-left py
1734 (- margin-left 3) py
1735 :stroke-color axes-color)
1736 (when (or (eq grid 'xy) (eq grid 'y))
1737 (svg-line svg margin-left py
1738 (- width margin-right) py
1739 :opacity grid-opacity
1740 :stroke-color grid-color))
1741 (when (zerop (e% y y-label-step))
1742 (svg-text svg (elt y-labels lnum)
1743 :font-family label-font
1745 :font-size label-font-size
1747 :x (- margin-left 6)
1748 :y (+ py (/ text-height 2) -1))
1751 (defun eplot--text-width (text font font-size &optional font-weight)
1753 (propertize text 'face
1754 (list :font (font-spec :family font
1755 :weight (or font-weight 'normal)
1756 :size font-size)))))
1758 (defvar eplot--text-size-cache (make-hash-table :test #'equal))
1760 (defun eplot--text-height (text font font-size &optional font-weight)
1761 (cdr (eplot--text-size text font font-size font-weight)))
1763 (defun eplot--text-size (text font font-size font-weight)
1764 (let ((key (list text font font-size font-weight)))
1765 (or (gethash key eplot--text-size-cache)
1766 (let ((size (eplot--text-size-1 text font font-size font-weight)))
1767 (setf (gethash key eplot--text-size-cache) size)
1770 (defun eplot--text-size-1 (text font font-size font-weight)
1771 (if (not (executable-find "convert"))
1772 ;; This "default" text size is kinda bogus.
1773 (cons (* (length text) font-size) font-size)
1774 (let* ((size (* font-size 10))
1775 (svg (svg-create size size))
1777 (svg-rectangle svg 0 0 size size :fill "black")
1780 :text-anchor "middle"
1781 :font-size font-size
1782 :font-weight (or font-weight 'normal)
1787 (set-buffer-multibyte nil)
1789 (let* ((file (concat (make-temp-name "/tmp/eplot") ".svg"))
1790 (png (file-name-with-extension file ".png")))
1793 (write-region (point-min) (point-max) file nil 'silent)
1794 ;; rsvg-convert is 5x faster than convert when doing SVG, so
1795 ;; if we have it, we use it.
1796 (when (executable-find "rsvg-convert")
1798 (call-process "rsvg-convert" nil nil nil
1799 (format "--output=%s" png) file)
1800 (when (file-exists-p png)
1804 (when (zerop (call-process "convert" nil t nil
1805 "-trim" "+repage" file "info:-"))
1806 (goto-char (point-min))
1807 (when (re-search-forward " \\([0-9]+\\)x\\([0-9]+\\)" nil t)
1809 (cons (string-to-number (match-string 1))
1810 (string-to-number (match-string 2)))))))
1811 (when (file-exists-p file)
1812 (delete-file file)))))
1814 ;; This "default" text size is kinda bogus.
1815 (cons (* (length text) font-size) font-size)))))
1817 (defun eplot--draw-legend (svg chart)
1818 (with-slots ( legend plots
1819 margin-left margin-top
1820 font font-size font-weight
1821 background-color axes-color
1822 legend-color legend-background-color legend-border-color)
1824 (when (eq legend 'true)
1826 (cl-loop for plot in plots
1827 for name = (slot-value plot 'name)
1830 (cons name (slot-value plot 'color)))))
1831 (svg-rectangle svg (+ margin-left 20) (+ margin-top 20)
1834 (seq-max (mapcar (lambda (name)
1835 (length (car name)))
1837 (* font-size (+ (length names) 2))
1838 :font-size font-size
1839 :fill-color legend-background-color
1840 :stroke-color legend-border-color)
1841 (cl-loop for name in names
1843 do (svg-text svg (car name)
1845 :text-anchor "front"
1846 :font-size font-size
1847 :font-weight font-weight
1848 :fill (or (cdr name) legend-color)
1849 :x (+ margin-left 25)
1850 :y (+ margin-top 40 (* i font-size))))))))
1852 (defun eplot--format-y (y spacing whole format-string)
1853 (format (or format-string "%s")
1855 ((or (= (round (* spacing 100)) 10) (= (round (* spacing 100)) 20))
1861 ((and (< spacing 1) (not (zerop (mod (* spacing 10) 1))))
1863 ((zerop (% spacing 1000000000))
1864 (format "%dG" (/ y 1000000000)))
1865 ((zerop (% spacing 1000000))
1866 (format "%dM" (/ y 1000000)))
1867 ((zerop (% spacing 1000))
1868 (format "%dk" (/ y 1000)))
1876 (defun eplot--format-value (value print-format label-format)
1877 (replace-regexp-in-string
1878 ;; Texts in SVG collapse multiple spaces into one. So do it here,
1879 ;; too, so that width calculations are correct.
1882 ((eq print-format 'date)
1884 (or label-format "%Y-%m-%d") (eplot--days-to-time value)))
1885 ((eq print-format 'year)
1886 (format-time-string (or label-format "%Y") (eplot--days-to-time value)))
1887 ((eq print-format 'time)
1888 (format-time-string (or label-format "%H:%M:%S") value))
1889 ((eq print-format 'minute)
1890 (format-time-string (or label-format "%H:%M") value))
1891 ((eq print-format 'hour)
1892 (format-time-string (or label-format "%H") value))
1894 (format (or label-format "%s") value)))))
1896 (defun eplot--compute-x-ticks (xs x-values print-format x-label-format
1897 label-font label-font-size)
1898 (let* ((min (seq-min x-values))
1899 (max (seq-max x-values))
1900 (count (length x-values))
1901 (max-print (eplot--format-value max print-format x-label-format))
1902 ;; We want each label to be spaced at least as long apart as
1903 ;; the length of the longest label, with room for two blanks
1905 (min-spacing (* 1.2 (eplot--text-width max-print label-font
1907 (digits (eplot--decimal-digits (- (cadr x-values) (car x-values))))
1908 (every (e/ 1 (expt 10 digits))))
1910 ;; We have room for every X value.
1911 ((< (* count min-spacing) xs)
1913 ;; We have to prune X labels, but not grid lines. (We shouldn't
1914 ;; have a grid line more than every 10 pixels.)
1915 ((< (* count 10) xs)
1917 (let ((label-step every))
1918 (while (> (/ (- max min) label-step) (/ xs min-spacing))
1919 (setq label-step (eplot--next-weed label-step)))
1921 ;; We have to reduce both grid lines and labels.
1923 (let ((tick-step every))
1924 (while (> (/ (- max min) tick-step) (/ xs 10))
1925 (setq tick-step (eplot--next-weed tick-step)))
1927 (let ((label-step tick-step))
1928 (while (> (/ (- max min) label-step) (/ xs min-spacing))
1929 (setq label-step (eplot--next-weed label-step))
1930 (while (not (zerop (% label-step tick-step)))
1931 (setq label-step (eplot--next-weed label-step))))
1934 (defun eplot--compute-y-ticks (ys y-values text-height)
1935 (let* ((min (car y-values))
1936 (max (car (last y-values)))
1937 (count (length y-values))
1938 ;; We want each label to be spaced at least as long apart as
1939 ;; the height of the label.
1940 (min-spacing (+ text-height 10))
1941 (digits (eplot--decimal-digits (- (cadr y-values) (car y-values))))
1942 (every (e/ 1 (expt 10 digits))))
1944 ;; We have room for every X value.
1945 ((< (* count min-spacing) ys)
1947 ;; We have to prune Y labels, but not grid lines. (We shouldn't
1948 ;; have a grid line more than every 10 pixels.)
1949 ((< (* count 10) ys)
1951 (let ((label-step every))
1952 (while (> (/ (- max min) label-step) (/ ys min-spacing))
1953 (setq label-step (eplot--next-weed label-step)))
1955 ;; We have to reduce both grid lines and labels.
1957 (let ((tick-step 1))
1958 (while (> (/ count tick-step) (/ ys 10))
1959 (setq tick-step (eplot--next-weed tick-step)))
1961 (let ((label-step tick-step))
1962 (while (> (/ count label-step) (/ ys min-spacing))
1963 (setq label-step (eplot--next-weed label-step))
1964 (while (not (zerop (% label-step tick-step)))
1965 (setq label-step (eplot--next-weed label-step))))
1968 (defvar eplot--pleasing-numbers '(1 2 5 10))
1970 (defun eplot--next-weed (weed)
1971 (let (digits series)
1973 (setq digits (truncate (log weed 10))
1974 series (/ weed (expt 10 digits)))
1975 (setq digits (eplot--decimal-digits weed)
1976 series (truncate (* weed (expt 10 digits)))))
1977 (let ((next (cadr (memq series eplot--pleasing-numbers))))
1979 (error "Invalid weed: %s" weed))
1981 (* next (expt 10 digits))
1982 (e/ next (expt 10 digits))))))
1984 (defun eplot--parse-gradient (string)
1986 (let ((bits (split-string string)))
1988 (cons 'from (nth 0 bits))
1989 (cons 'to (nth 1 bits))
1990 (cons 'direction (intern (or (nth 2 bits) "top-down")))
1991 (cons 'position (intern (or (nth 3 bits) "below")))))))
1993 (defun eplot--smooth (values algo xs)
1996 (let* ((vals (cl-coerce values 'vector))
1997 (max (1- (length vals)))
1998 (period (* 4 (ceiling (/ max xs)))))
2001 (cl-loop for i from 0 upto max
2002 collect (e/ (cl-loop for ii from 0 upto (1- period)
2003 sum (elt vals (min (+ i ii) max)))
2006 (defun eplot--vary-color (color n)
2007 (let ((colors ["#e6194b" "#3cb44b" "#ffe119" "#4363d8" "#f58231" "#911eb4"
2008 "#46f0f0" "#f032e6" "#bcf60c" "#fabebe" "#008080" "#e6beff"
2009 "#9a6324" "#fffac8" "#800000" "#aaffc3" "#808000" "#ffd8b1"
2010 "#000075" "#808080" "#ffffff" "#000000"]))
2011 (unless (equal color "vary")
2013 (if (string-search " " color)
2014 (split-string color)
2016 (elt colors (mod n (length colors)))))
2018 (defun eplot--pv (plot slot &optional default)
2019 (let ((user (cdr (assq slot eplot--user-defaults))))
2020 (when (and (stringp user) (zerop (length user)))
2022 (or user (slot-value plot slot) default)))
2024 (defun eplot--draw-plots (svg chart)
2025 (if (eq (slot-value chart 'format) 'horizontal-bar-chart)
2026 (eplot--draw-horizontal-bar-chart svg chart)
2027 (eplot--draw-normal-plots svg chart)))
2029 (defun eplot--draw-normal-plots (svg chart)
2030 (with-slots ( plots chart-color height format
2031 margin-bottom margin-left
2034 x-values x-min x-max
2035 label-font label-font-size)
2037 ;; Draw all the plots.
2038 (cl-loop for plot in (reverse plots)
2039 for plot-number from 0
2040 for values = (slot-value plot 'values)
2041 for stride = (eplot--stride chart values)
2042 for vals = (eplot--smooth
2043 (seq-map (lambda (v) (plist-get v :value)) values)
2044 (slot-value plot 'smoothing)
2047 for gradient = (eplot--parse-gradient (eplot--pv plot 'gradient))
2050 for style = (if (eq format 'bar-chart)
2052 (slot-value plot 'style))
2053 for bar-gap = (* stride 0.1)
2054 for clip-id = (format "url(#clip-%d)" plot-number)
2059 `((id . ,(format "clip-%d" plot-number)))
2061 `((x . ,margin-left)
2066 (when-let ((fill (slot-value plot 'fill-color)))
2067 (setq gradient `((from . ,fill) (to . ,fill)
2068 (direction . top-down) (position . below)))))
2070 (if (eq (eplot--vs 'position gradient) 'above)
2071 (push (cons margin-left margin-top) polygon)
2072 (push (cons margin-left (- height margin-bottom)) polygon)))
2078 for settings = (plist-get value :settings)
2079 for color = (eplot--vary-color
2080 (eplot--vs 'color settings (slot-value plot 'color))
2082 for py = (- (- height margin-bottom)
2083 (* (/ (- (* 1.0 val) min) (- max min))
2085 for px = (if (eq style 'bar)
2087 (* (e/ (- x x-min) (- x-max x-min -1))
2090 (* (e/ (- x x-min) (- x-max x-min))
2093 ;; Some data points may have texts.
2094 (when-let ((text (eplot--vs 'text settings)))
2096 :font-family label-font
2097 :text-anchor "middle"
2098 :font-size label-font-size
2099 :font-weight 'normal
2102 :y (- py (eplot--text-height
2103 text label-font label-font-size)
2105 ;; You may mark certain points.
2106 (when-let ((mark (eplot--vy 'mark settings)))
2109 (let ((s (eplot--element-size val plot settings 3)))
2110 (svg-line svg (- px s) (- py s)
2114 (svg-line svg (+ px s) (- py s)
2119 (svg-circle svg px py 3
2125 svg (+ px bar-gap) py
2126 (- stride bar-gap) (- height margin-bottom py)
2129 (let ((id (format "gradient-%s" (make-temp-name "grad"))))
2130 (eplot--gradient svg id 'linear
2131 (eplot--stops (eplot--vs 'from gradient)
2132 (eplot--vs 'to gradient))
2133 (eplot--vs 'direction gradient))
2135 svg (+ px bar-gap) py
2136 (- stride bar-gap) (- height margin-bottom py)
2140 (let ((width (eplot--element-size val plot settings 1)))
2144 px (- height margin-bottom)
2148 (- px (e/ width 2)) py
2149 width (- height py margin-bottom)
2153 (svg-line svg px py (1+ px) (1+ py)
2157 ;; If we're doing a gradient, we're just collecting
2158 ;; points and will draw the polygon later.
2160 (push (cons px py) polygon)
2162 (svg-line svg lpx lpy px py
2163 :stroke-width (eplot--pv plot 'size 1)
2167 (push (cons px py) polygon))
2172 (push (cons lpx py) polygon))
2173 (push (cons px py) polygon))
2175 (svg-line svg lpx lpy px lpy
2178 (svg-line svg px lpy px py
2182 (svg-circle svg px py
2183 (eplot--element-size val plot settings 3)
2186 :fill (eplot--vary-color
2188 'fill-color settings
2189 (or (slot-value plot 'fill-color) "none"))
2192 (let ((s (eplot--element-size val plot settings 3)))
2193 (svg-line svg (- px s) (- py s)
2197 (svg-line svg (+ px s) (- py s)
2202 (let ((s (eplot--element-size val plot settings 5)))
2205 (cons (- px (e/ s 2)) (+ py (e/ s 2)))
2206 (cons px (- py (e/ s 2)))
2207 (cons (+ px (e/ s 2)) (+ py (e/ s 2))))
2211 (or (slot-value plot 'fill-color) "none"))))
2213 (let ((s (eplot--element-size val plot settings 3)))
2214 (svg-rectangle svg (- px (e/ s 2)) (- py (e/ s 2))
2219 (or (slot-value plot 'fill-color) "none")))))
2223 ;; We're doing a gradient of some kind (or a curve), so
2224 ;; draw it now when we've collected the polygon.
2226 ;; We have a "between" chart, so collect the data points
2227 ;; from the "extra" values, too.
2228 (when (memq 'two-values (slot-value plot 'data-format))
2230 for val in (nreverse
2231 (seq-map (lambda (v) (plist-get v :extra-value))
2233 for x from (1- (length vals)) downto 0
2234 for py = (- (- height margin-bottom)
2235 (* (/ (- (* 1.0 val) min) (- max min))
2237 for px = (+ margin-left
2238 (* (e/ (- x x-min) (- x-max x-min))
2243 (push (cons px py) polygon))
2246 (push (cons lpx py) polygon))
2247 (push (cons px py) polygon)))
2248 (setq lpx px lpy py)))
2250 (if (eq (eplot--vs 'position gradient) 'above)
2251 (push (cons lpx margin-top) polygon)
2252 (push (cons lpx (- height margin-bottom)) polygon)))
2253 (let ((id (format "gradient-%d" plot-number)))
2255 (eplot--gradient svg id 'linear
2256 (eplot--stops (eplot--vs 'from gradient)
2257 (eplot--vs 'to gradient))
2258 (eplot--vs 'direction gradient)))
2259 (if (eq style 'curve)
2260 (apply #'svg-path svg
2263 with points = (cl-coerce
2264 (nreverse polygon) 'vector)
2265 for i from 0 upto (1- (length points))
2269 `(moveto ((,(car (elt points 0)) .
2270 ,(cdr (elt points 0))))))
2274 (eplot--pv plot 'bezier-factor)
2276 (and gradient '((closepath))))
2277 `( :clip-path ,clip-id
2278 :stroke-width ,(eplot--pv plot 'size 1)
2279 :stroke ,(slot-value plot 'color)
2284 svg (nreverse polygon)
2287 :stroke (slot-value plot 'fill-border-color))))))))
2289 (defun eplot--element-size (value plot settings default)
2290 (eplot--vn 'size settings
2291 (if (slot-value plot 'size-factor)
2292 (* value (slot-value plot 'size-factor))
2293 (or (slot-value plot 'size) default))))
2295 (defun eplot--draw-horizontal-bar-chart (svg chart)
2296 (with-slots ( plots chart-color height format
2297 margin-bottom margin-left
2300 x-values x-min x-max
2301 label-font label-font-size label-color)
2303 (cl-loop with plot = (car plots)
2304 with values = (slot-value plot 'values)
2305 with stride = (e/ ys (length values))
2306 with label-height = (eplot--text-height "xx" label-font
2308 with bar-gap = (* stride 0.1)
2311 for settings = (plist-get value :settings)
2312 for py = (+ margin-top (* i stride))
2313 for px = (* (e/ (plist-get value :x) x-max) xs)
2314 for color = (eplot--vary-color
2315 (eplot--vs 'color settings (slot-value plot 'color))
2318 (svg-text svg (eplot--vs 'label settings)
2319 :font-family label-font
2321 :font-size label-font-size
2322 :font-weight 'normal
2325 :y (+ py label-height (/ (- stride label-height) 2)))
2327 margin-left (+ py (e/ bar-gap 2))
2328 px (- stride bar-gap)
2331 (defun eplot--stops (from to)
2332 (append `((0 . ,from))
2333 (cl-loop for (pct col) on (split-string to "-") by #'cddr
2335 (cons (string-to-number pct) col)
2338 (defun eplot--gradient (svg id type stops &optional direction)
2339 "Add a gradient with ID to SVG.
2340 TYPE is `linear' or `radial'.
2342 STOPS is a list of percentage/color pairs.
2344 DIRECTION is one of `top-down', `bottom-up', `left-right' or `right-left'.
2345 nil means `top-down'."
2350 (if (eq type 'linear)
2354 (x1 . ,(if (eq direction 'left-right) 1 0))
2355 (x2 . ,(if (eq direction 'right-left) 1 0))
2356 (y1 . ,(if (eq direction 'bottom-up) 1 0))
2357 (y2 . ,(if (eq direction 'top-down) 1 0)))
2360 (dom-node 'stop `((offset . ,(format "%s%%" (car stop)))
2361 (stop-color . ,(cdr stop)))))
2364 (defun e% (num1 num2)
2365 (let ((factor (max (expt 10 (eplot--decimal-digits num1))
2366 (expt 10 (eplot--decimal-digits num2)))))
2367 (% (truncate (* num1 factor)) (truncate (* num2 factor)))))
2369 (defun eplot--decimal-digits (number)
2370 (- (length (replace-regexp-in-string
2372 (format "%.10f" (- number (truncate number)))))
2375 (defun e/ (&rest numbers)
2376 (if (cl-every #'integerp numbers)
2377 (let ((int (apply #'/ numbers))
2378 (float (apply #'/ (* 1.0 (car numbers)) (cdr numbers))))
2382 (apply #'/ numbers)))
2384 (defun eplot--get-ticks (min max height &optional whole)
2385 (let* ((diff (abs (- min max)))
2386 (even (eplot--pleasing-numbers (* (e/ diff height) 10)))
2387 (factor (max (expt 10 (eplot--decimal-digits even))
2388 (expt 10 (eplot--decimal-digits diff))))
2389 (fmin (truncate (* min factor)))
2390 (feven (truncate (* factor even)))
2401 (- (% (floor fmin) feven))
2404 (- fmin (% fmin feven)))))
2405 (cl-loop for x from start upto (* max factor) by feven
2406 collect (e/ x factor))))
2408 (defun eplot--days-to-time (days)
2409 (days-to-time (- days (time-to-days 0))))
2411 (defun eplot--get-date-ticks (start end xs label-font label-font-size
2412 x-label-format &optional skip-until)
2413 (let* ((duration (- end start))
2416 (list (/ 368 16) 'date
2418 (list (/ 368 4) 'date
2421 (= (decoded-time-weekday decoded) 1)))
2422 (list (/ 368 2) 'date
2423 ;; Collect 1st and 15th.
2425 (or (= (decoded-time-day decoded) 1)
2426 (= (decoded-time-day decoded) 15))))
2427 (list (* 368 2) 'date
2428 ;; Collect 1st of every month.
2430 (= (decoded-time-day decoded) 1)))
2431 (list (* 368 4) 'date
2432 ;; Collect every quarter.
2434 (and (= (decoded-time-day decoded) 1)
2435 (memq (decoded-time-month decoded) '(1 4 7 10)))))
2436 (list (* 368 8) 'date
2437 ;; Collect every half year.
2439 (and (= (decoded-time-day decoded) 1)
2440 (memq (decoded-time-month decoded) '(1 7)))))
2441 (list 1.0e+INF 'year
2442 ;; Collect every Jan 1st.
2444 (and (= (decoded-time-day decoded) 1)
2445 (= (decoded-time-month decoded) 1)))))))
2446 ;; First we collect the potential ticks.
2447 (while (or (>= duration (caar limits))
2448 (and skip-until (>= skip-until (caar limits))))
2450 (let* ((x-ticks (cl-loop for day from start upto end
2451 for time = (eplot--days-to-time day)
2452 for decoded = (decode-time time)
2453 when (funcall (nth 2 (car limits)) decoded)
2455 (count (length x-ticks))
2456 (print-format (nth 1 (car limits)))
2457 (max-print (eplot--format-value (car x-ticks) print-format
2459 (min-spacing (* 1.2 (eplot--text-width max-print label-font
2462 ;; We have room for every X value.
2463 ((< (* count min-spacing) xs)
2464 (list x-ticks print-format))
2465 ;; We have to prune X labels, but not grid lines. (We shouldn't
2466 ;; have a grid line more than every 10 pixels.)
2467 ((< (* count 10) xs)
2471 x-ticks xs label-font label-font-size x-label-format))
2472 ;; The Mondays grid is special, because it doesn't resolve
2473 ;; into any of the bigger limits evenly.
2474 ((= (caar limits) (/ 368 4))
2475 (let* ((max-print (eplot--format-value
2476 (car x-ticks) print-format x-label-format))
2477 (min-spacing (* 1.2 (eplot--text-width
2478 max-print label-font label-font-size)))
2480 (while (> (* (/ (length x-ticks) weed-factor) min-spacing) xs)
2481 (setq weed-factor (* weed-factor 2)))
2483 (cl-loop for val in x-ticks
2485 collect (list val t (zerop (% i weed-factor)))))))
2491 (cl-loop for day in x-ticks
2492 for time = (eplot--days-to-time day)
2493 for decoded = (decode-time time)
2496 (funcall (nth 2 (car limits))
2498 (setq print-format (nth 1 (car limits)))
2499 (let* ((max-print (eplot--format-value
2500 (car x-ticks) print-format x-label-format))
2501 (min-spacing (* 1.2 (eplot--text-width
2502 max-print label-font
2504 (num-labels (seq-count (lambda (v) (nth 2 v))
2506 (when (and (not (zerop num-labels))
2507 (< (* num-labels min-spacing) xs))
2508 (throw 'found (list x-ticks print-format candidate)))))
2511 x-ticks xs label-font label-font-size x-label-format)))))
2512 ;; We have to reduce both grid lines and labels.
2514 (eplot--get-date-ticks start end xs label-font label-font-size
2515 x-label-format (caar limits)))))))
2517 (defun eplot--year-ticks (x-ticks xs label-font label-font-size x-label-format)
2518 (let* ((year-ticks (mapcar (lambda (day)
2520 (decode-time (eplot--days-to-time day))))
2522 (xv (eplot--compute-x-ticks
2523 xs year-ticks 'year x-label-format label-font label-font-size)))
2524 (let ((tick-step (car xv))
2525 (label-step (cadr xv)))
2527 (cl-loop for year in year-ticks
2530 (zerop (% year tick-step))
2531 (zerop (% year label-step))))))))
2533 (defun eplot--get-time-ticks (start end xs label-font label-font-size
2535 &optional skip-until)
2536 (let* ((duration (- end start))
2539 (list (* 2 60) 'time
2541 (list (* 2 60 60) 'time
2542 ;; Collect whole minutes.
2544 (zerop (decoded-time-second decoded))))
2545 (list (* 3 60 60) 'minute
2546 ;; Collect five minutes.
2548 (zerop (% (decoded-time-minute decoded) 5))))
2549 (list (* 4 60 60) 'minute
2550 ;; Collect fifteen minutes.
2552 (and (zerop (decoded-time-second decoded))
2553 (memq (decoded-time-minute decoded) '(0 15 30 45)))))
2554 (list (* 8 60 60) 'minute
2555 ;; Collect half hours.
2557 (and (zerop (decoded-time-second decoded))
2558 (memq (decoded-time-minute decoded) '(0 30)))))
2559 (list 1.0e+INF 'hour
2560 ;; Collect whole hours.
2562 (and (zerop (decoded-time-second decoded))
2563 (zerop (decoded-time-minute decoded))))))))
2564 ;; First we collect the potential ticks.
2565 (while (or (>= duration (caar limits))
2566 (and skip-until (>= skip-until (caar limits))))
2568 (let* ((x-ticks (cl-loop for time from start upto end
2569 for decoded = (decode-time time)
2570 when (funcall (nth 2 (car limits)) decoded)
2572 (count (length x-ticks))
2573 (print-format (nth 1 (car limits)))
2574 (max-print (eplot--format-value (car x-ticks) print-format
2576 (min-spacing (* (+ (length max-print) 2) (e/ label-font-size 2))))
2578 ;; We have room for every X value.
2579 ((< (* count min-spacing) xs)
2580 (list x-ticks print-format))
2581 ;; We have to prune X labels, but not grid lines. (We shouldn't
2582 ;; have a grid line more than every 10 pixels.)
2583 ;; If we're plotting just seconds, then just weed out some seconds.
2584 ((and (< (* count 10) xs)
2585 (= (caar limits) (* 2 60)))
2586 (let ((xv (eplot--compute-x-ticks
2587 xs x-ticks 'time x-label-format label-font label-font-size)))
2588 (let ((tick-step (car xv))
2589 (label-step (cadr xv)))
2591 (cl-loop for val in x-ticks
2593 (zerop (% val tick-step))
2594 (zerop (% val label-step))))))))
2595 ;; Normal case for pruning labels, but not grid lines.
2596 ((< (* count 10) xs)
2597 (if (not (cdr limits))
2598 (eplot--hour-ticks x-ticks xs label-font label-font-size
2604 (cl-loop for val in x-ticks
2605 for decoded = (decode-time val)
2608 (funcall (nth 2 (car limits))
2610 (setq print-format (nth 1 (car limits)))
2611 (let ((min-spacing (* (+ (length max-print) 2)
2612 (e/ label-font-size 2))))
2613 (when (< (* (seq-count (lambda (v) (nth 2 v)) candidate)
2616 (throw 'found (list x-ticks print-format candidate)))))
2618 (eplot--hour-ticks x-ticks xs label-font label-font-size
2620 ;; We have to reduce both grid lines and labels.
2622 (eplot--get-time-ticks start end xs label-font label-font-size
2623 x-label-format (caar limits)))))))
2625 (defun eplot--hour-ticks (x-ticks xs label-font label-font-size
2627 (let* ((eplot--pleasing-numbers '(1 3 6 12))
2628 (hour-ticks (mapcar (lambda (time)
2629 (decoded-time-hour (decode-time time)))
2631 (xv (eplot--compute-x-ticks
2632 xs hour-ticks 'year x-label-format label-font label-font-size)))
2633 (let ((tick-step (car xv))
2634 (label-step (cadr xv)))
2636 (cl-loop for hour in hour-ticks
2639 (zerop (% hour tick-step))
2640 (zerop (% hour label-step))))))))
2642 (defun eplot--int (number)
2646 ((= number (truncate number))
2651 (defun eplot--pleasing-numbers (number)
2652 (let* ((digits (eplot--decimal-digits number))
2653 (one (e/ 1 (expt 10 digits)))
2654 (two (e/ 2 (expt 10 digits)))
2655 (five (e/ 5 (expt 10 digits))))
2658 (when (< number one)
2660 (setq one (* one 10))
2661 (when (< number two)
2663 (setq two (* two 10))
2664 (when (< number five)
2665 (throw 'found five))
2666 (setq five (* five 10))))))
2668 (defun eplot-parse-and-insert (file)
2669 "Parse and insert a file in the current buffer."
2670 (interactive "fEplot file: ")
2671 (let ((default-directory (file-name-directory file)))
2672 (setq-local eplot--current-chart
2673 (eplot--render (with-temp-buffer
2674 (insert-file-contents file)
2675 (eplot--parse-buffer))))))
2677 (defun eplot-list-chart-headers ()
2678 "Pop to a buffer showing all chart parameters."
2680 (pop-to-buffer "*eplot help*")
2681 (let ((inhibit-read-only t))
2684 (insert "The following headers influence the overall\nlook of the chart:\n\n")
2685 (eplot--list-headers eplot--chart-headers)
2686 (ensure-empty-lines 2)
2687 (insert "The following headers are per plot:\n\n")
2688 (eplot--list-headers eplot--plot-headers)
2689 (goto-char (point-min))))
2691 (defun eplot--list-headers (headers)
2692 (dolist (header (sort (copy-sequence headers)
2694 (string< (car e1) (car e2)))))
2695 (insert (propertize (capitalize (symbol-name (car header))) 'face 'bold)
2697 (let ((start (point)))
2698 (insert (plist-get (cdr header) :doc) "\n")
2699 (when-let ((valid (plist-get (cdr header) :valid)))
2700 (insert "Possible values are: "
2701 (mapconcat (lambda (v) (format "`%s'" v)) valid ", ")
2703 (indent-rigidly start (point) 2))
2704 (ensure-empty-lines 1)))
2706 (defvar eplot--transients
2710 ("sl" "Margin-Left")
2712 ("sr" "Margin-Right")
2713 ("sb" "Margin-Bottom"))
2716 ("cb" "Border-Color")
2717 ("cc" "Chart-Color")
2718 ("cf" "Frame-Color")
2719 ("cs" "Surround-Color")
2720 ("ct" "Title-Color"))
2722 ("bc" "Background-Color")
2723 ("bg" "Background-Gradient")
2724 ("bi" "Background-Image-File")
2725 ("bv" "Background-Image-Cover")
2726 ("bo" "Background-Image-Opacity")))
2731 ("ge" "Font-Weight")
2733 ("gw" "Frame-Width")
2734 ("gh" "Header-File")
2738 ("gr" "Reset" eplot--reset-transient)
2739 ("gv" "Save" eplot--save-transient))
2740 ("Axes, Grid & Legend"
2744 ("xz" "Label-Font-Size")
2745 ("xs" "X-Axis-Title-Space")
2746 ("xl" "X-Label-Format")
2747 ("xa" "Y-Label-Format")
2749 ("io" "Grid-Opacity")
2750 ("ip" "Grid-Position")
2752 ("lb" "Legend-Background-Color")
2753 ("lo" "Legend-Border-Color")
2754 ("lc" "Legend-Color"))
2758 ("po" "Data-Column")
2759 ("pr" "Data-format")
2760 ("pn" "Fill-Border-Color")
2765 ("pb" "Bezier-Factor")))))
2767 (defun eplot--define-transients ()
2768 (cl-loop for row in eplot--transients
2770 (cl-loop for column in row
2774 (mapcar #'eplot--define-transient column))
2778 (defun eplot--define-transient (action)
2779 (list (nth 0 action)
2781 ;; Allow explicit commands.
2783 ;; Make a command for altering a setting.
2786 (eplot--execute-transient (nth 1 action))))))
2788 (defun eplot--execute-transient (action)
2789 (with-current-buffer (or eplot--data-buffer (current-buffer))
2790 (unless eplot--transient-settings
2791 (setq-local eplot--transient-settings nil))
2792 (let* ((name (intern (downcase action)))
2793 (spec (assq name (append eplot--chart-headers eplot--plot-headers)))
2794 (type (plist-get (cdr spec) :type)))
2797 (error "No such header type: %s" name))
2798 (setq eplot--transient-settings
2800 eplot--transient-settings
2806 (read-number (format "Value for %s (%s): " action type)))
2807 ((string-match "color" (downcase action))
2808 (eplot--read-color (format "Value for %s (color): " action)))
2809 ((string-match "font" (downcase action))
2810 (eplot--read-font-family
2811 (format "Value for %s (font family): " action)))
2812 ((string-match "gradient" (downcase action))
2813 (eplot--read-gradient action))
2814 ((string-match "file" (downcase action))
2815 (read-file-name (format "File for %s: " action)))
2818 (completing-read (format "Value for %s: " action)
2819 (plist-get (cdr spec) :valid)
2822 (read-string (format "Value for %s (string): " action))))))))
2823 (eplot-update-view-buffer))))
2825 (defun eplot--read-gradient (action)
2826 (format "%s %s %s %s"
2827 (eplot--read-color (format "%s from color: " action))
2828 (eplot--read-color (format "%s to color: " action))
2829 (completing-read (format "%s direction: " action)
2830 '(top-down bottom-up left-right right-left)
2832 (completing-read (format "%s position: " action)
2836 (defun eplot--reset-transient ()
2838 (with-current-buffer (or eplot--data-buffer (current-buffer))
2839 (setq-local eplot--transient-settings nil)
2840 (eplot-update-view-buffer)))
2842 (defun eplot--save-transient (file)
2843 (interactive "FSave parameters to file: ")
2844 (when (and (file-exists-p file)
2845 (not (yes-or-no-p "File exists; overwrite? ")))
2846 (user-error "Exiting"))
2847 (let ((settings (with-current-buffer (or eplot--data-buffer (current-buffer))
2848 eplot--transient-settings)))
2850 (cl-loop for (name . value) in settings
2851 do (insert (capitalize (symbol-name name)) ": "
2852 (format "%s" value) "\n"))
2853 (write-region (point-min) (point-max) file))))
2855 (defvar-keymap eplot-control-mode-map
2856 "RET" #'eplot-control-update
2857 "TAB" #'eplot-control-next-field
2858 "C-<tab>" #'eplot-control-next-field
2859 "<backtab>" #'eplot-control-prev-field)
2861 (define-derived-mode eplot-control-mode special-mode "eplot control"
2862 (setq-local completion-at-point-functions
2863 (cons 'eplot--complete-control completion-at-point-functions))
2864 (add-hook 'before-change-functions #'eplot--process-text-input-before nil t)
2865 (add-hook 'after-change-functions #'eplot--process-text-value nil t)
2866 (add-hook 'after-change-functions #'eplot--process-text-input nil t)
2867 (setq-local nobreak-char-display nil)
2868 (setq truncate-lines t))
2870 (defun eplot--complete-control ()
2871 ;; Complete headers names.
2872 (when-let* ((input (get-text-property (point) 'input))
2873 (name (plist-get input :name))
2874 (spec (cdr (assq name (append eplot--plot-headers
2875 eplot--chart-headers))))
2876 (start (plist-get input :start))
2877 (end (- (plist-get input :end) 2))
2878 (completion-ignore-case t))
2879 (skip-chars-backward " " start)
2881 (and (eq (plist-get spec :type) 'symbol)
2883 (let ((valid (plist-get spec :valid)))
2884 (completion-in-region
2886 (skip-chars-backward "^ " start)
2889 (mapcar #'symbol-name valid))
2890 'completion-attempted)))
2891 (and (string-match "color" (symbol-name name))
2893 (completion-in-region
2895 (skip-chars-backward "^ " start)
2898 'completion-attempted))
2899 (and (string-match "\\bfile\\b" (symbol-name name))
2901 (completion-in-region
2903 (skip-chars-backward "^ " start)
2905 end (directory-files "."))
2906 'completion-attempted))
2907 (and (string-match "\\bfont\\b" (symbol-name name))
2909 (completion-in-region
2911 (skip-chars-backward "^ " start)
2914 (eplot--font-families))
2915 'completion-attempted)))))
2917 (defun eplot--read-font-family (prompt)
2918 "Prompt for a font family, possibly offering autocomplete."
2919 (let ((families (eplot--font-families)))
2921 (completing-read prompt families)
2922 (read-string prompt))))
2924 (defun eplot--font-families ()
2925 (when (executable-find "fc-list")
2928 (call-process "fc-list" nil t nil ":" "family")
2929 (goto-char (point-min))
2930 (while (re-search-forward "^\\([^,\n]+\\)" nil t)
2931 (push (downcase (match-string 1)) fonts)))
2932 (seq-uniq (sort fonts #'string<)))))
2934 (defun eplot-control-next-input ()
2935 "Go to the next input field."
2937 (when-let ((match (text-property-search-forward 'input)))
2938 (goto-char (prop-match-beginning match))))
2940 (defun eplot-control-update ()
2941 "Update the chart based on the current settings."
2943 (let ((settings nil))
2945 (goto-char (point-min))
2946 (while-let ((match (text-property-search-forward 'input)))
2947 (when (equal (get-text-property (prop-match-beginning match) 'face)
2948 'eplot--input-changed)
2949 (let* ((name (plist-get (prop-match-value match) :name))
2950 (spec (cdr (assq name (append eplot--plot-headers
2951 eplot--chart-headers))))
2953 (or (plist-get (prop-match-value match) :value)
2954 (plist-get (prop-match-value match) :original-value))))
2955 (setq value (string-trim (string-replace "\u00A0" " " value)))
2957 (cl-case (plist-get spec :type)
2959 (string-to-number value))
2961 (intern (downcase value)))
2963 (mapcar #'intern (split-string (downcase value))))
2967 (with-current-buffer eplot--data-buffer
2968 (setq-local eplot--transient-settings (nreverse settings))
2969 (eplot-update-view-buffer))))
2971 (defvar eplot--column-width nil)
2973 (defun eplot-create-controls ()
2974 "Pop to a buffer that lists all parameters and allows editing."
2976 (with-current-buffer (or eplot--data-buffer (current-buffer))
2977 (let ((settings eplot--transient-settings)
2978 (data-buffer (current-buffer))
2979 (chart eplot--current-chart)
2980 ;; Find the max width of all the different names.
2986 (apply #'append eplot--transients))))))
2987 (transients (mapcar #'copy-sequence
2988 (copy-sequence eplot--transients))))
2990 (user-error "Must be called from an eplot buffer that has rendered a chart"))
2991 ;; Rearrange the transients a bit for better display.
2992 (let ((size (caar transients)))
2993 (setcar (car transients) (caadr transients))
2994 (setcar (cadr transients) size))
2995 (pop-to-buffer "*eplot controls*")
2996 (unless (eq major-mode 'eplot-control-mode)
2997 (eplot-control-mode))
2998 (setq-local eplot--data-buffer data-buffer
2999 eplot--column-width (+ width 12 2))
3000 (let ((inhibit-read-only t)
3001 (before-change-functions nil)
3002 (after-change-functions nil))
3004 (cl-loop for column in transients
3007 (goto-char (point-min))
3014 (insert (format (format "%%-%ds" (+ width 14)) "")
3016 (unless (= (count-lines (point-min) (point)) 1)
3019 (insert (format (format "%%-%ds" (+ width 14)) "")
3021 (insert (format (format "%%-%ds" (+ width 14)) "")
3027 ;; If we have a too-long input in the first column,
3028 ;; then go to the next line.
3030 (> (- (point) (pos-bol))
3034 (insert (format (format "%%-%ds" (+ width 14))
3035 (propertize (pop row) 'face 'bold)))
3036 (if (looking-at "\n")
3041 for name = (cadr elem)
3042 for slot = (intern (downcase name))
3043 when (null (nth 2 elem))
3045 (let* ((object (if (assq slot eplot--chart-headers)
3047 (car (slot-value chart 'plots))))
3049 (or (cdr (assq slot settings))
3050 (if (not (slot-boundp object slot))
3052 (or (slot-value object slot)
3055 ;; If we have a too-long input in the first column,
3056 ;; then go to the next line.
3058 (> (- (point) (pos-bol))
3064 (insert (format (format "%%-%ds" (+ width 14)) "") "\n")
3067 (insert (format (format "%%-%ds" (1+ width)) name))
3068 (eplot--input slot value
3069 (if (cdr (assq slot settings))
3070 'eplot--input-changed
3071 'eplot--input-default))
3072 (if (looking-at "\n")
3075 (goto-char (point-min)))))
3077 (defface eplot--input-default
3078 '((t :background "#505050"
3079 :foreground "#a0a0a0"
3080 :box (:line-width 1)))
3081 "Face for eplot default inputs.")
3083 (defface eplot--input-changed
3084 '((t :background "#505050"
3086 :box (:line-width 1)))
3087 "Face for eplot changed inputs.")
3089 (defvar-keymap eplot--input-map
3090 :full t :parent text-mode-map
3091 "RET" #'eplot-control-update
3092 "TAB" #'eplot-input-complete
3093 "C-a" #'eplot-move-beginning-of-input
3094 "C-e" #'eplot-move-end-of-input
3095 "C-k" #'eplot-kill-input
3096 "C-<tab>" #'eplot-control-next-field
3097 "<backtab>" #'eplot-control-prev-field)
3099 (defun eplot-input-complete ()
3100 "Complete values in inputs."
3103 ((let ((completion-fail-discreetly t))
3104 (completion-at-point))
3105 ;; Completion was performed; nothing else to do.
3107 ((not (get-text-property (point) 'input))
3108 (eplot-control-next-input))
3110 (user-error "No completion in this field"))))
3112 (defun eplot-move-beginning-of-input ()
3113 "Move to the start of the current input field."
3115 (if (= (point) (eplot--beginning-of-field))
3116 (goto-char (pos-bol))
3117 (goto-char (eplot--beginning-of-field))))
3119 (defun eplot-move-end-of-input ()
3120 "Move to the end of the current input field."
3122 (let ((input (get-text-property (point) 'input)))
3124 (= (point) (1- (plist-get input :end))))
3125 (goto-char (pos-eol))
3126 (goto-char (1+ (eplot--end-of-field))))))
3128 (defun eplot-control-next-field ()
3129 "Move to the beginning of the next field."
3131 (let ((input (get-text-property (point) 'input))
3134 (goto-char (plist-get input :end)))
3135 (let ((match (text-property-search-forward 'input)))
3137 (goto-char (prop-match-beginning match))
3139 (user-error "No next field")))))
3141 (defun eplot-control-prev-field ()
3142 "Move to the beginning of the previous field."
3144 (let ((input (get-text-property (point) 'input))
3147 (goto-char (plist-get input :start))
3150 (let ((match (text-property-search-backward 'input)))
3153 (user-error "No previous field")))))
3155 (defun eplot-kill-input ()
3156 "Remove the part of the input after point."
3158 (let ((end (1+ (eplot--end-of-field))))
3159 (kill-new (string-trim (buffer-substring (point) end)))
3160 (delete-region (point) end)))
3162 (defun eplot--input (name value face)
3163 (let ((start (point))
3166 (when (< (length value) 11)
3167 (insert (make-string (- 11 (length value)) ?\u00A0)))
3168 (put-text-property start (point) 'face face)
3169 (put-text-property start (point) 'inhibit-read-only t)
3170 (put-text-property start (point) 'input
3174 :is-default (eq face 'eplot--input-default)
3175 :original-value value
3177 :start (set-marker (make-marker) start)
3179 (put-text-property start (point) 'local-map eplot--input-map)
3180 ;; This seems like a NOOP, but redoing the properties like this
3181 ;; somehow makes `delete-region' work better.
3182 (set-text-properties start (point) (text-properties-at start))
3183 (insert (propertize " " 'face face
3185 'inhibit-read-only t
3186 'local-map eplot--input-map))
3187 (plist-put input :end (point-marker))
3190 (defun eplot--end-of-field ()
3191 (- (plist-get (get-text-property (point) 'input) :end) 2))
3193 (defun eplot--beginning-of-field ()
3194 (plist-get (get-text-property (point) 'input) :start))
3196 (defvar eplot--prev-deletion nil)
3198 (defun eplot--process-text-input-before (beg end)
3199 (message "Before: %s %s" beg end)
3202 (setq eplot--prev-deletion nil))
3204 (setq eplot--prev-deletion (buffer-substring beg end)))))
3206 (defun eplot--process-text-input (beg end _replace-length)
3207 ;;(message "After: %s %s %s %s" beg end replace-length eplot--prev-deletion)
3208 (when-let ((props (if eplot--prev-deletion
3209 (text-properties-at 0 eplot--prev-deletion)
3210 (if (get-text-property end 'input)
3211 (text-properties-at end)
3212 (text-properties-at beg))))
3213 (input (plist-get props 'input)))
3214 ;; The action concerns something in the input field.
3215 (let ((buffer-undo-list t)
3216 (inhibit-read-only t)
3217 (size (plist-get input :size)))
3219 (set-text-properties beg (- (plist-get input :end) 2) props)
3220 (goto-char (1- (plist-get input :end)))
3221 (let* ((remains (- (point) (plist-get input :start) 1))
3222 (trim (- size remains 1)))
3223 (if (< remains size)
3224 ;; We need to add some padding.
3225 (insert (apply #'propertize (make-string trim ?\u00A0)
3227 ;; We need to delete some padding, but only delete
3228 ;; spaces at the end.
3229 (setq trim (abs trim))
3230 (while (and (> trim 0)
3231 (eql (char-after (1- (point))) ?\u00A0))
3232 (delete-region (1- (point)) (point))
3235 (eplot--possibly-open-column)))))
3236 ;; We re-set the properties so that they are continguous. This
3237 ;; somehow makes the machinery that decides whether we can kill
3238 ;; a word work better.
3239 (set-text-properties (plist-get input :start)
3240 (1- (plist-get input :end)) props)
3241 ;; Compute what the value is now.
3242 (let ((value (buffer-substring-no-properties
3243 (plist-get input :start)
3244 (plist-get input :end))))
3245 (when (string-match "\u00A0+\\'" value)
3246 (setq value (substring value 0 (match-beginning 0))))
3247 (plist-put input :value value)))))
3249 (defun eplot--possibly-open-column ()
3251 (when-let ((input (get-text-property (point) 'input)))
3252 (goto-char (plist-get input :end)))
3253 (unless (looking-at " *\n")
3254 (skip-chars-forward " ")
3256 (let ((text (buffer-substring (point) (pos-eol))))
3257 (delete-region (point) (pos-eol))
3260 (insert (make-string eplot--column-width ?\s) text "\n")
3261 (forward-char eplot--column-width)
3262 (if (get-text-property (point) 'input)
3265 ;; We have to fix up the markers.
3267 (let* ((match (text-property-search-backward 'input))
3268 (input (prop-match-value match)))
3269 (plist-put input :start
3270 (set-marker (plist-get input :start)
3271 (prop-match-beginning match)))
3272 (plist-put input :end
3273 (set-marker (plist-get input :end)
3274 (+ (prop-match-end match) 1))))))))))))
3276 (defun eplot--process-text-value (beg _end _replace-length)
3277 (when-let* ((input (get-text-property beg 'input)))
3278 (let ((inhibit-read-only t))
3279 (when (plist-get input :is-default)
3280 (put-text-property (plist-get input :start)
3281 (plist-get input :end)
3283 (if (equal (plist-get input :original-value)
3284 (plist-get input :value))
3285 'eplot--input-default
3286 'eplot--input-changed))))))
3288 (defun eplot--read-color (prompt)
3289 "Read an SVG color."
3290 (completing-read prompt eplot--colors))
3292 (eval `(transient-define-prefix eplot-customize ()
3294 ,@(eplot--define-transients)))
3296 (defun eplot--bezier (factor i points)
3297 (cl-labels ((padd (p1 p2)
3298 (cons (+ (car p1) (car p2)) (+ (cdr p1) (cdr p2))))
3300 (cons (- (car p1) (car p2)) (- (cdr p1) (cdr p2))))
3301 (pscale (factor point)
3302 (cons (* factor (car point)) (* factor (cdr point)))))
3303 (let* ((start (elt points (1- i)))
3304 (end (elt points i))
3305 (prev (if (< (- i 2) 0)
3307 (elt points (- i 2))))
3308 (next (if (> (1+ i) (1- (length points)))
3310 (elt points (1+ i))))
3311 (start-control-point
3312 (padd start (pscale factor (psub end prev))))
3314 (padd end (pscale factor (psub start next)))))
3315 (list (car start-control-point)
3316 (cdr start-control-point)
3317 (car end-control-point)
3318 (cdr end-control-point)
3322 ;;; CSV Parsing Stuff.
3324 (defun eplot--csv-buffer-p ()
3326 (goto-char (point-min))
3327 (let ((min 1.0e+INF)
3333 (while (search-forward "," (pos-eol) t)
3338 (setq min (min min this)
3339 max (max max this))))
3340 (let ((mid (e/ total lines)))
3341 ;; If we have a comma on each line, and it's fairly evenly
3342 ;; distributed, it's a CSV buffer.
3345 (> (* mid 1.1) max))))))
3347 (defun eplot--numericalp (value)
3348 (string-match-p "\\`[-.0-9]*\\'" value))
3350 (defun eplot--numberish (value)
3351 (if (or (zerop (length value))
3352 (not (eplot--numericalp value)))
3354 (string-to-number value)))
3356 (defun eplot--parse-csv-buffer ()
3357 (unless (fboundp 'pcsv-parse-buffer)
3358 (user-error "You need to install the pcsv package to parse CSV files"))
3359 (let ((csv (and (fboundp 'pcsv-parse-buffer)
3360 ;; This repeated check is just to silence the byte
3362 (pcsv-parse-buffer)))
3364 ;; Check whether the first line looks like a header.
3365 (when (and (length> csv 1)
3366 ;; The second line is all numbers...
3367 (cl-every #'eplot--numericalp (nth 1 csv))
3368 ;; .. and the first line isn't.
3369 (not (cl-every #'eplot--numericalp (nth 0 csv))))
3370 (setq names (pop csv)))
3372 (cons 'legend (and names "true"))
3375 for column from 1 upto (1- (length (car csv)))
3377 (list (cons :headers
3379 (cons 'name (elt names column))
3382 ((cl-every (lambda (e) (<= (length e) 4))
3385 ((cl-every (lambda (e) (= (length e) 8))
3390 (cons 'color (eplot--vary-color "vary" (1- column)))))
3393 (cl-loop for line in csv
3394 collect (list :x (eplot--numberish (car line))
3395 :value (eplot--numberish
3396 (elt line column)))))))))))
3398 (declare-function org-element-parse-buffer "org-element")
3400 (defun eplot--parse-org-buffer ()
3401 (require 'org-element)
3402 (let* ((table (nth 2 (nth 2 (org-element-parse-buffer))))
3403 (columns (cl-loop for cell in (nthcdr 2 (nth 2 table))
3404 collect (substring-no-properties (nth 2 cell))))
3405 (value-column (or (seq-position columns "value") 0))
3406 (date-column (seq-position columns "date")))
3409 ,@(and date-column '((data-format . "date"))))
3411 ,@(cl-loop for row in (nthcdr 4 table)
3413 (let ((cells (cl-loop for cell in (nthcdr 2 row)
3414 collect (substring-no-properties
3416 (list :value (string-to-number (elt cells value-column))
3417 :x (string-to-number
3418 (replace-regexp-in-string
3419 "[^0-9]" "" (elt cells date-column)))
3424 ;;; eplot.el ends here