1.1--- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2+++ b/.emacs.d/lib/eplot.el Sun Sep 08 20:46:10 2024 -0400
1.3@@ -0,0 +1,3424 @@
1.4+;;; eplot.el --- Manage and Edit Wordpress Posts -*- lexical-binding: t -*-
1.5+
1.6+;; Copyright (C) 2024 Free Software Foundation, Inc.
1.7+
1.8+;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org>
1.9+;; Keywords: charts
1.10+;; Package: eplot
1.11+;; Version: 1.0
1.12+;; Package-Requires: ((emacs "29.0.59") (pcsv "0.0"))
1.13+
1.14+;; eplot is free software; you can redistribute it and/or modify it
1.15+;; under the terms of the GNU General Public License as published by
1.16+;; the Free Software Foundation; either version 2, or (at your option)
1.17+;; any later version.
1.18+
1.19+;;; Commentary:
1.20+
1.21+;; The main entry point is `M-x eplot' in a buffer with time series
1.22+;; data.
1.23+;;
1.24+;; If installing manually, put something like the following in your
1.25+;; Emacs init file (but adjust the path to where you've put eplot):
1.26+;;
1.27+;; (push "~/src/eplot/" load-path)
1.28+;; (autoload 'eplot "eplot" nil t)
1.29+;; (autoload 'eplot-mode "eplot" nil t)
1.30+;; (unless (assoc "\\.plt" auto-mode-alist)
1.31+;; (setq auto-mode-alist (cons '("\\.plt" . eplot-mode) auto-mode-alist)))
1.32+
1.33+;; This requires the pcsv package to parse CSV files.
1.34+
1.35+;;; Code:
1.36+
1.37+(require 'svg)
1.38+(require 'cl-lib)
1.39+(require 'face-remap)
1.40+(require 'eieio)
1.41+(require 'iso8601)
1.42+(require 'transient)
1.43+
1.44+(defvar eplot--user-defaults nil)
1.45+(defvar eplot--chart-headers nil)
1.46+(defvar eplot--plot-headers nil)
1.47+(defvar eplot--transient-settings nil)
1.48+
1.49+
1.50+(defvar eplot--colors
1.51+ '("aliceblue" "antiquewhite" "aqua" "aquamarine" "azure" "beige" "bisque"
1.52+ "black" "blanchedalmond" "blue" "blueviolet" "brown" "burlywood"
1.53+ "cadetblue" "chartreuse" "chocolate" "coral" "cornflowerblue" "cornsilk"
1.54+ "crimson" "cyan" "darkblue" "darkcyan" "darkgoldenrod" "darkgray"
1.55+ "darkgreen" "darkgrey" "darkkhaki" "darkmagenta" "darkolivegreen"
1.56+ "darkorange" "darkorchid" "darkred" "darksalmon" "darkseagreen"
1.57+ "darkslateblue" "darkslategray" "darkslategrey" "darkturquoise"
1.58+ "darkviolet" "deeppink" "deepskyblue" "dimgray" "dimgrey" "dodgerblue"
1.59+ "firebrick" "floralwhite" "forestgreen" "fuchsia" "gainsboro" "ghostwhite"
1.60+ "gold" "goldenrod" "gray" "green" "greenyellow" "grey" "honeydew" "hotpink"
1.61+ "indianred" "indigo" "ivory" "khaki" "lavender" "lavenderblush" "lawngreen"
1.62+ "lemonchiffon" "lightblue" "lightcoral" "lightcyan" "lightgoldenrodyellow"
1.63+ "lightgray" "lightgreen" "lightgrey" "lightpink" "lightsalmon"
1.64+ "lightseagreen" "lightskyblue" "lightslategray" "lightslategrey"
1.65+ "lightsteelblue" "lightyellow" "lime" "limegreen" "linen" "magenta"
1.66+ "maroon" "mediumaquamarine" "mediumblue" "mediumorchid" "mediumpurple"
1.67+ "mediumseagreen" "mediumslateblue" "mediumspringgreen" "mediumturquoise"
1.68+ "mediumvioletred" "midnightblue" "mintcream" "mistyrose" "moccasin"
1.69+ "navajowhite" "navy" "oldlace" "olive" "olivedrab" "orange" "orangered"
1.70+ "orchid" "palegoldenrod" "palegreen" "paleturquoise" "palevioletred"
1.71+ "papayawhip" "peachpuff" "peru" "pink" "plum" "powderblue" "purple" "red"
1.72+ "rosybrown" "royalblue" "saddlebrown" "salmon" "sandybrown" "seagreen"
1.73+ "seashell" "sienna" "silver" "skyblue" "slateblue" "slategray" "slategrey"
1.74+ "snow" "springgreen" "steelblue" "tan" "teal" "thistle" "tomato"
1.75+ "turquoise" "violet" "wheat" "white" "whitesmoke" "yellow" "yellowgreen"))
1.76+
1.77+(defun eplot-set (header value)
1.78+ "Set the default value of HEADER to VALUE.
1.79+To get a list of all possible HEADERs, use the `M-x
1.80+eplot-list-chart-headers' command.
1.81+
1.82+Also see `eplot-reset'."
1.83+ (let ((elem (or (assq header eplot--chart-headers)
1.84+ (assq header eplot--plot-headers))))
1.85+ (unless elem
1.86+ (error "No such header type: %s" header))
1.87+ (eplot--add-default header value)))
1.88+
1.89+(defun eplot--add-default (header value)
1.90+ ;; We want to preserve the order defaults have been added, so that
1.91+ ;; we can apply them in the same order. This makes a difference
1.92+ ;; when we're dealing with specs that have inheritence.
1.93+ (setq eplot--user-defaults (delq (assq header eplot--user-defaults)
1.94+ eplot--user-defaults))
1.95+ (setq eplot--user-defaults (list (cons header value))))
1.96+
1.97+(defun eplot-reset (&optional header)
1.98+ "Reset HEADER to defaults.
1.99+If HEADER is nil or not present, reset everything to defaults."
1.100+ (if header
1.101+ (setq eplot--user-defaults (delq (assq header eplot--user-defaults)
1.102+ eplot--user-defaults))
1.103+ (setq eplot--user-defaults nil)))
1.104+
1.105+(unless (assoc "\\.plt" auto-mode-alist)
1.106+ (setq auto-mode-alist (cons '("\\.plt" . eplot-mode) auto-mode-alist)))
1.107+
1.108+;;; eplot modes.
1.109+
1.110+(defvar-keymap eplot-mode-map
1.111+ "C-c C-c" #'eplot-update-view-buffer
1.112+ "C-c C-p" #'eplot-switch-view-buffer
1.113+ "C-c C-e" #'eplot-list-chart-headers
1.114+ "C-c C-v" #'eplot-customize
1.115+ "C-c C-l" #'eplot-create-controls
1.116+ "TAB" #'eplot-complete)
1.117+
1.118+;; # is working overtime in the syntax here:
1.119+;; It can be a color like Color: #e0e0e0, and
1.120+;; it can be a setting like 33 # Label: Apples,
1.121+;; when it starts a line it's a comment.
1.122+(defvar eplot-font-lock-keywords
1.123+ `(("^[ \t\n]*#.*" . font-lock-comment-face)
1.124+ ("^[^ :\n]+:" . font-lock-keyword-face)
1.125+ ("#[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)
1.126+ ("#.*" . font-lock-builtin-face)))
1.127+
1.128+(define-derived-mode eplot-mode text-mode "eplot"
1.129+ "Major mode for editing charts.
1.130+Use the \\[eplot-list-chart-headers] command to get a list of all
1.131+possible chart headers."
1.132+ (setq-local completion-at-point-functions
1.133+ (cons 'eplot--complete-header completion-at-point-functions))
1.134+ (setq-local font-lock-defaults
1.135+ '(eplot-font-lock-keywords nil nil nil)))
1.136+
1.137+(defun eplot-complete ()
1.138+ "Complete headers."
1.139+ (interactive)
1.140+ (cond
1.141+ ((let ((completion-fail-discreetly t))
1.142+ (completion-at-point))
1.143+ ;; Completion was performed; nothing else to do.
1.144+ nil)
1.145+ (t (indent-relative))))
1.146+
1.147+(defun eplot--complete-header ()
1.148+ (or
1.149+ ;; Complete headers names.
1.150+ (and (or (looking-at ".*:")
1.151+ (and (looking-at "[ \t]*$")
1.152+ (save-excursion
1.153+ (beginning-of-line)
1.154+ (not (looking-at "\\(.+\\):")))))
1.155+ (lambda ()
1.156+ (let ((headers (mapcar
1.157+ (lambda (h)
1.158+ (if (looking-at ".*:")
1.159+ (capitalize (symbol-name (car h)))
1.160+ (concat (capitalize (symbol-name (car h))) ": ")))
1.161+ (save-excursion
1.162+ ;; If we're after the headers, then we want
1.163+ ;; to complete over the plot headers. Otherwise,
1.164+ ;; complete over the chart headers.
1.165+ (if (and (not (bobp))
1.166+ (progn
1.167+ (forward-line -1)
1.168+ (re-search-backward "^[ \t]*$" nil t)))
1.169+ eplot--plot-headers
1.170+ eplot--chart-headers))))
1.171+ (completion-ignore-case t))
1.172+ (completion-in-region (pos-bol) (line-end-position) headers)
1.173+ 'completion-attempted)))
1.174+ ;; Complete header values.
1.175+ (let ((hname nil))
1.176+ (and (save-excursion
1.177+ (and (looking-at "[ \t]*$")
1.178+ (progn
1.179+ (beginning-of-line)
1.180+ (and (looking-at "\\(.+\\):")
1.181+ (setq hname (intern (downcase (match-string 1)))))))
1.182+ (lambda ()
1.183+ (let ((valid (plist-get
1.184+ (cdr (assq hname (append eplot--plot-headers
1.185+ eplot--chart-headers)))
1.186+ :valid))
1.187+ (completion-ignore-case t))
1.188+ (completion-in-region
1.189+ (save-excursion
1.190+ (search-backward ":" (pos-bol) t)
1.191+ (skip-chars-forward ": \t")
1.192+ (point))
1.193+ (line-end-position)
1.194+ (mapcar #'symbol-name valid))
1.195+ 'completion-attempted)))))))
1.196+
1.197+(define-minor-mode eplot-minor-mode
1.198+ "Minor mode to issue commands from an eplot data buffer."
1.199+ :lighter " eplot")
1.200+
1.201+(defvar-keymap eplot-minor-mode-map
1.202+ "H-l" #'eplot-eval-and-update)
1.203+
1.204+(defvar-keymap eplot-view-mode-map
1.205+ "s" #'eplot-view-write-file
1.206+ "w" #'eplot-view-write-scaled-file
1.207+ "c" #'eplot-view-customize
1.208+ "l" #'eplot-create-controls)
1.209+
1.210+(define-derived-mode eplot-view-mode special-mode "eplot view"
1.211+ "Major mode for displaying eplots."
1.212+ (setq-local revert-buffer-function #'eplot-update
1.213+ cursor-type nil))
1.214+
1.215+(defun eplot-view-write-file (file &optional width)
1.216+ "Write the current chart to a file.
1.217+If you type in a file name that ends with something else than \"svg\",
1.218+ImageMagick \"convert\" will be used to convert the image first.
1.219+
1.220+If writing to a PNG file, \"rsvg-conver\" will be used instead if
1.221+it exists as this usually gives better results."
1.222+ (interactive "FWrite to file name: ")
1.223+ (when (and (file-exists-p file)
1.224+ (not (yes-or-no-p "File exists, overwrite? ")))
1.225+ (error "Not overwriting the file"))
1.226+ (save-excursion
1.227+ (goto-char (point-min))
1.228+ (let ((match
1.229+ (text-property-search-forward 'display nil
1.230+ (lambda (_ e)
1.231+ (and (consp e)
1.232+ (eq (car e) 'image))))))
1.233+ (unless match
1.234+ (error "Can't find an image in the current buffer"))
1.235+ (let ((svg (plist-get (cdr (prop-match-value match)) :data))
1.236+ (tmp " *eplot convert*")
1.237+ (executable (if width "rsvg-convert" "convert"))
1.238+ sfile ofile)
1.239+ (unless svg
1.240+ (error "Invalid image in the current buffer"))
1.241+ (with-temp-buffer
1.242+ (set-buffer-multibyte nil)
1.243+ (svg-print svg)
1.244+ (if (string-match-p "\\.svg\\'" file)
1.245+ (write-region (point-min) (point-max) file)
1.246+ (if (and (string-match-p "\\.png\\'" file)
1.247+ (executable-find "rsvg-convert"))
1.248+ (setq executable "rsvg-convert")
1.249+ (unless (executable-find executable)
1.250+ (error "%s isn't installed; can only save svg files"
1.251+ executable)))
1.252+ (when (and (equal executable "rsvg-convert")
1.253+ (not (string-match-p "\\.png\\'" file))
1.254+ (not (executable-find "convert")))
1.255+ (error "Can only write PNG files when scaling because \"convert\" isn't installed"))
1.256+ (unwind-protect
1.257+ (progn
1.258+ (setq sfile (make-temp-file "eplot" nil ".svg")
1.259+ ofile (make-temp-file "eplot" nil ".png"))
1.260+ (write-region (point-min) (point-max) sfile nil 'silent)
1.261+ ;; We don't use `call-process-region', because
1.262+ ;; convert doesn't seem to like that?
1.263+ (let ((code (if (equal executable "rsvg-convert")
1.264+ (apply
1.265+ #'call-process
1.266+ executable nil (get-buffer-create tmp) nil
1.267+ `(,(format "--output=%s"
1.268+ (expand-file-name ofile))
1.269+ ,@(and width
1.270+ `(,(format "--width=%d" width)
1.271+ "--keep-aspect-ratio"))
1.272+ ,sfile))
1.273+ (call-process
1.274+ executable nil (get-buffer-create tmp) nil
1.275+ sfile file))))
1.276+ (eplot--view-error code tmp)
1.277+ (when (file-exists-p ofile)
1.278+ (if (string-match-p "\\.png\\'" file)
1.279+ (rename-file ofile file)
1.280+ (let ((code (call-process "convert" nil tmp nil
1.281+ ofile file)))
1.282+ (eplot--view-error code tmp))))
1.283+ (message "Wrote %s" file)))
1.284+ ;; Clean-up.
1.285+ (when (get-buffer tmp)
1.286+ (kill-buffer tmp))
1.287+ (when (file-exists-p sfile)
1.288+ (delete-file sfile))
1.289+ (when (file-exists-p ofile)
1.290+ (delete-file sfile)))))))))
1.291+
1.292+(defun eplot--view-error (code tmp)
1.293+ (unless (zerop code)
1.294+ (error "Error code %d: %s"
1.295+ code
1.296+ (with-current-buffer tmp
1.297+ (while (search-forward "[ \t\n]+" nil t)
1.298+ (replace-match " "))
1.299+ (string-trim (buffer-string))))))
1.300+
1.301+(defun eplot-view-write-scaled-file (width file)
1.302+ "Write the current chart to a rescaled to a file.
1.303+The rescaling is done by \"rsvg-convert\", which has to be
1.304+installed. Rescaling is done when rendering, so this should give
1.305+you a clear, non-blurry version of the chart at any size."
1.306+ (interactive "nWidth: \nFWrite to file: ")
1.307+ (eplot-view-write-file file width))
1.308+
1.309+(defun eplot-view-customize ()
1.310+ "Customize the settings for the chart in the current buffer."
1.311+ (interactive)
1.312+ (with-suppressed-warnings ((interactive-only eplot-customize))
1.313+ (eplot-customize)))
1.314+
1.315+(defvar eplot--data-buffer nil)
1.316+(defvar eplot--current-chart nil)
1.317+
1.318+(defun eplot ()
1.319+ "Plot the data in the current buffer."
1.320+ (interactive)
1.321+ (eplot-update-view-buffer))
1.322+
1.323+(defun eplot-with-headers (header-file)
1.324+ "Plot the data in the current buffer using headers from a file."
1.325+ (interactive "fHeader file: ")
1.326+ (eplot-update-view-buffer
1.327+ (with-temp-buffer
1.328+ (insert-file-contents header-file)
1.329+ (eplot--parse-headers))))
1.330+
1.331+(defun eplot-switch-view-buffer ()
1.332+ "Switch to the eplot view buffer and render the chart."
1.333+ (interactive)
1.334+ (eplot-update-view-buffer nil t))
1.335+
1.336+(defun eplot-update-view-buffer (&optional headers switch)
1.337+ "Update the eplot view buffer based on the current data buffer."
1.338+ (interactive)
1.339+ ;; This is mainly useful during implementation.
1.340+ (if (and (eq major-mode 'emacs-lisp-mode)
1.341+ (get-buffer-window "*eplot*" t))
1.342+ (with-current-buffer "*eplot*"
1.343+ (eplot-update)
1.344+ (when-let ((win (get-buffer-window "*eplot*" t)))
1.345+ (set-window-point win (point-min))))
1.346+ ;; Normal case.
1.347+ (let* ((eplot--user-defaults (eplot--settings-table))
1.348+ (data (eplot--parse-buffer))
1.349+ (data-buffer (current-buffer))
1.350+ (window (selected-window)))
1.351+ (unless data
1.352+ (user-error "No data in the current buffer"))
1.353+ (setq data (eplot--inject-headers data headers))
1.354+ (if (get-buffer-window "*eplot*" t)
1.355+ (set-buffer "*eplot*")
1.356+ (if switch
1.357+ (pop-to-buffer-same-window "*eplot*")
1.358+ (pop-to-buffer "*eplot*")))
1.359+ (let ((inhibit-read-only t))
1.360+ (erase-buffer)
1.361+ (unless (eq major-mode 'eplot-view-mode)
1.362+ (eplot-view-mode))
1.363+ (setq-local eplot--data-buffer data-buffer)
1.364+ (let ((chart (eplot--render data)))
1.365+ (with-current-buffer data-buffer
1.366+ (setq-local eplot--current-chart chart)))
1.367+ (insert "\n")
1.368+ (when-let ((win (get-buffer-window "*eplot*" t)))
1.369+ (set-window-point win (point-min))))
1.370+ (select-window window))))
1.371+
1.372+(defun eplot--settings-table ()
1.373+ (if (not eplot--transient-settings)
1.374+ eplot--user-defaults
1.375+ (append eplot--user-defaults eplot--transient-settings)))
1.376+
1.377+(defun eplot--inject-headers (data headers)
1.378+ ;; It's OK not to separate the plot headers from the chart
1.379+ ;; headers. Collect them here, if any.
1.380+ (when-let ((plot-headers
1.381+ (cl-loop for elem in (mapcar #'car eplot--plot-headers)
1.382+ for value = (eplot--vs elem headers)
1.383+ when value
1.384+ collect (progn
1.385+ ;; Remove these headers from the data
1.386+ ;; headers so that we don't get errors
1.387+ ;; on undefined headers.
1.388+ (setq headers (delq (assq elem headers)
1.389+ headers))
1.390+ (cons elem value)))))
1.391+ (dolist (plot (cdr (assq :plots data)))
1.392+ (let ((headers (assq :headers plot)))
1.393+ (if headers
1.394+ (nconc headers plot-headers)
1.395+ (nconc plot (list (list :headers plot-headers)))))))
1.396+ (append data headers))
1.397+
1.398+(defun eplot-eval-and-update ()
1.399+ "Helper command when developing."
1.400+ (interactive nil emacs-lisp-mode)
1.401+ (save-some-buffers t)
1.402+ (elisp-eval-region-or-buffer)
1.403+ (eval-defun nil)
1.404+ (eplot-update-view-buffer))
1.405+
1.406+;;; Parsing buffers.
1.407+
1.408+(defun eplot-update (&rest _ignore)
1.409+ "Update the plot in the current buffer."
1.410+ (interactive)
1.411+ (unless eplot--data-buffer
1.412+ (user-error "No data buffer associated with this eplot view buffer"))
1.413+ (let ((data (with-current-buffer eplot--data-buffer
1.414+ (eplot--parse-buffer)))
1.415+ (eplot--user-defaults (with-current-buffer eplot--data-buffer
1.416+ (eplot--settings-table)))
1.417+ (inhibit-read-only t))
1.418+ (erase-buffer)
1.419+ (let ((chart (eplot--render data)))
1.420+ (with-current-buffer eplot--data-buffer
1.421+ (setq-local eplot--current-chart chart)))
1.422+ (insert "\n\n")))
1.423+
1.424+(defun eplot--parse-buffer ()
1.425+ (if (eq major-mode 'org-mode)
1.426+ (eplot--parse-org-buffer)
1.427+ (eplot--parse-eplot-buffer)))
1.428+
1.429+(defun eplot--parse-eplot-buffer ()
1.430+ (if (eplot--csv-buffer-p)
1.431+ (eplot--parse-csv-buffer)
1.432+ (let ((buf (current-buffer)))
1.433+ (with-temp-buffer
1.434+ (insert-buffer-substring buf)
1.435+ ;; Remove comments first.
1.436+ (goto-char (point-min))
1.437+ (while (re-search-forward "^[ \t]*#" nil t)
1.438+ (delete-line))
1.439+ (goto-char (point-min))
1.440+ ;; First headers.
1.441+ (let* ((data (eplot--parse-headers))
1.442+ (plot-headers
1.443+ ;; It's OK not to separate the plot headers from the chart
1.444+ ;; headers. Collect them here, if any.
1.445+ (cl-loop for elem in (mapcar #'car eplot--plot-headers)
1.446+ for value = (eplot--vs elem data)
1.447+ when value
1.448+ collect (progn
1.449+ ;; Remove these headers from the data
1.450+ ;; headers so that we don't get errors
1.451+ ;; on undefined headers.
1.452+ (setq data (delq (assq elem data) data))
1.453+ (cons elem value))))
1.454+ plots)
1.455+ ;; Then the values.
1.456+ (while-let ((plot (eplot--parse-values nil plot-headers)))
1.457+ (setq plot-headers nil)
1.458+ (push plot plots))
1.459+ (when plots
1.460+ (push (cons :plots (nreverse plots)) data))
1.461+ data)))))
1.462+
1.463+(defun eplot--parse-headers ()
1.464+ (let ((data nil)
1.465+ type value)
1.466+ (while (looking-at "\\([^\n\t :]+\\):\\(.*\\)")
1.467+ (setq type (intern (downcase (match-string 1)))
1.468+ value (substring-no-properties (string-trim (match-string 2))))
1.469+ (forward-line 1)
1.470+ ;; Get continuation lines.
1.471+ (while (looking-at "[ \t]+\\(.*\\)")
1.472+ (setq value (concat value " " (string-trim (match-string 1))))
1.473+ (forward-line 1))
1.474+ (if (eq type 'header-file)
1.475+ (setq data (nconc data
1.476+ (with-temp-buffer
1.477+ (insert-file-contents value)
1.478+ (eplot--parse-headers))))
1.479+ ;; We don't use `push' here because we want to preserve order
1.480+ ;; also when inserting headers from other files.
1.481+ (setq data (nconc data (list (cons type value))))))
1.482+ data))
1.483+
1.484+(defun eplot--parse-values (&optional in-headers data-headers)
1.485+ ;; Skip past separator lines.
1.486+ (while (looking-at "[ \t]*\n")
1.487+ (forward-line 1))
1.488+ (let* ((values nil)
1.489+ ;; We may have plot-specific headers.
1.490+ (headers (nconc (eplot--parse-headers) data-headers))
1.491+ (data-format (or (eplot--vyl 'data-format headers)
1.492+ (eplot--vyl 'data-format in-headers)))
1.493+ (two-values (memq 'two-values data-format))
1.494+ (xy (or (memq 'year data-format)
1.495+ (memq 'date data-format)
1.496+ (memq 'time data-format)
1.497+ (memq 'xy data-format)))
1.498+ (data-column (or (eplot--vn 'data-column headers)
1.499+ (eplot--vn 'data-column in-headers))))
1.500+ (if-let ((data-file (eplot--vs 'data-file headers)))
1.501+ (with-temp-buffer
1.502+ (insert-file-contents data-file)
1.503+ (setq values (cdr (assq :values (eplot--parse-values headers)))
1.504+ headers (delq (assq 'data headers) headers)))
1.505+ ;; Now we come to the data. The data is typically either just a
1.506+ ;; number, or two numbers (in which case the first number is a
1.507+ ;; date or a time). Labels ans settings can be introduced with
1.508+ ;; a # char.
1.509+ (while (looking-at "\\([-0-9. \t]+\\)\\([ \t]*#\\(.*\\)\\)?")
1.510+ (let ((numbers (match-string 1))
1.511+ (settings (eplot--parse-settings (match-string 3)))
1.512+ this)
1.513+ (setq numbers (mapcar #'string-to-number
1.514+ (split-string (string-trim numbers))))
1.515+ ;; If we're reading two dimensionalish data, the first
1.516+ ;; number is the date/time/x.
1.517+ (when xy
1.518+ (setq this (list :x (pop numbers))))
1.519+ ;; Chop off all the numbers until we read the column(s)
1.520+ ;; we're using.
1.521+ (when data-column
1.522+ (setq numbers (nthcdr (1- data-column) numbers)))
1.523+ (when numbers
1.524+ (setq this (nconc this (list :value (pop numbers)))))
1.525+ (when two-values
1.526+ (setq this (nconc this (list :extra-value (pop numbers)))))
1.527+ (when settings
1.528+ (setq this (nconc this (list :settings settings))))
1.529+ (when (plist-get this :value)
1.530+ (push this values)))
1.531+ (forward-line 1))
1.532+ (setq values (nreverse values)))
1.533+ (and values
1.534+ `((:headers . ,headers) (:values . ,values)))))
1.535+
1.536+(defun eplot--parse-settings (string)
1.537+ (when string
1.538+ (with-temp-buffer
1.539+ (insert (string-trim string) "\n")
1.540+ (goto-char (point-min))
1.541+ (while (re-search-forward "\\(.\\)," nil t)
1.542+ (if (equal (match-string 1) "\\")
1.543+ (replace-match "," t t)
1.544+ (delete-char -1)
1.545+ (insert "\n")
1.546+ (when (looking-at "[ \t]+")
1.547+ (replace-match ""))))
1.548+ (goto-char (point-min))
1.549+ (eplot--parse-headers))))
1.550+
1.551+;;; Accessing data.
1.552+
1.553+(defun eplot--vn (type data &optional default)
1.554+ (if-let ((value (cdr (assq type data))))
1.555+ (string-to-number value)
1.556+ default))
1.557+
1.558+(defun eplot--vs (type data &optional default)
1.559+ (or (cdr (assq type data)) default))
1.560+
1.561+(defun eplot--vy (type data &optional default)
1.562+ (if-let ((value (cdr (assq type data))))
1.563+ (intern (downcase value))
1.564+ default))
1.565+
1.566+(defun eplot--vyl (type data &optional default)
1.567+ (if-let ((value (cdr (assq type data))))
1.568+ (mapcar #'intern (split-string (downcase value)))
1.569+ default))
1.570+
1.571+(defmacro eplot-def (args doc-string)
1.572+ (declare (indent defun))
1.573+ `(eplot--def ',(nth 0 args) ',(nth 1 args) ',(nth 2 args) ',(nth 3 args)
1.574+ ,doc-string))
1.575+
1.576+(defun eplot--def (name type default valid doc)
1.577+ (setq eplot--chart-headers (delq (assq name eplot--chart-headers)
1.578+ eplot--chart-headers))
1.579+ (push (list name
1.580+ :type type
1.581+ :default default
1.582+ :doc doc
1.583+ :valid valid)
1.584+ eplot--chart-headers))
1.585+
1.586+(eplot-def (width number)
1.587+ "The width of the entire chart.")
1.588+
1.589+(eplot-def (height number)
1.590+ "The height of the entire chart.")
1.591+
1.592+(eplot-def (format symbol normal (normal bar-chart horizontal-bar-chart))
1.593+ "The overall format of the chart.")
1.594+
1.595+(eplot-def (layout symbol nil (normal compact))
1.596+ "The general layout of the chart.")
1.597+
1.598+(eplot-def (mode symbol light (dark light))
1.599+ "Dark/light mode.")
1.600+
1.601+(eplot-def (margin-left number 70)
1.602+ "The left margin.")
1.603+
1.604+(eplot-def (margin-right number 20)
1.605+ "The right margin.")
1.606+
1.607+(eplot-def (margin-top number 40)
1.608+ "The top margin.")
1.609+
1.610+(eplot-def (margin-bottom number 60)
1.611+ "The bottom margin.")
1.612+
1.613+(eplot-def (x-axis-title-space number 5)
1.614+ "The space between the X axis and the label.")
1.615+
1.616+(eplot-def (font string "sans-serif")
1.617+ "The font to use in titles, labels and legends.")
1.618+
1.619+(eplot-def (font-size number 12)
1.620+ "The font size.")
1.621+
1.622+(eplot-def (font-weight symbol bold (bold normal))
1.623+ "The font weight.")
1.624+
1.625+(eplot-def (label-font string (spec font))
1.626+ "The font to use for axes labels.")
1.627+
1.628+(eplot-def (label-font-size number (spec font-size))
1.629+ "The font size to use for axes labels.")
1.630+
1.631+(eplot-def (bar-font string (spec font))
1.632+ "The font to use for bar chart labels.")
1.633+
1.634+(eplot-def (bar-font-size number (spec font-size))
1.635+ "The font size to use for bar chart labels.")
1.636+
1.637+(eplot-def (bar-font-weight symbol (spec font-weight) (bold normal))
1.638+ "The font weight to use for bar chart labels.")
1.639+
1.640+(eplot-def (chart-color string "black")
1.641+ "The foreground color to use in plots, axes, legends, etc.
1.642+This is used as the default, but can be overridden per thing.")
1.643+
1.644+(eplot-def (background-color string "white")
1.645+ "The background color.
1.646+If you want a chart with a transparent background, use the color
1.647+\"none\".")
1.648+
1.649+(eplot-def (background-gradient string)
1.650+ "Use this to get a gradient color in the background.")
1.651+
1.652+(eplot-def (axes-color string (spec chart-color))
1.653+ "The color of the axes.")
1.654+
1.655+(eplot-def (grid-color string "#e0e0e0")
1.656+ "The color of the grid.")
1.657+
1.658+(eplot-def (grid symbol xy (xy x y off))
1.659+ "What grid axes to do.")
1.660+
1.661+(eplot-def (grid-opacity number)
1.662+ "The opacity of the grid.
1.663+This should either be nil or a value between 0 and 1, where 0 is
1.664+fully transparent.")
1.665+
1.666+(eplot-def (grid-position symbol bottom (bottom top))
1.667+ "Whether to put the grid on top or under the plot.")
1.668+
1.669+(eplot-def (legend symbol nil (true nil))
1.670+ "Whether to do a legend.")
1.671+
1.672+(eplot-def (legend-color string (spec chart-color))
1.673+ "The color of legends (if any).")
1.674+
1.675+(eplot-def (legend-border-color string (spec chart-color))
1.676+ "The border color of legends (if any).")
1.677+
1.678+(eplot-def (legend-background-color string (spec background-color))
1.679+ "The background color of legends (if any).")
1.680+
1.681+(eplot-def (label-color string (spec axes-color))
1.682+ "The color of labels on the axes.")
1.683+
1.684+(eplot-def (surround-color string)
1.685+ "The color between the plot area and the edges of the chart.")
1.686+
1.687+(eplot-def (border-color string)
1.688+ "The color of the border of the chart, if any.")
1.689+
1.690+(eplot-def (border-width number)
1.691+ "The width of the border of the chart, if any.")
1.692+
1.693+(eplot-def (frame-color string)
1.694+ "The color of the frame of the plot, if any.")
1.695+
1.696+(eplot-def (frame-width number)
1.697+ "The width of the frame of the plot, if any.")
1.698+
1.699+(eplot-def (min number)
1.700+ "The minimum value in the chart.
1.701+This is normally computed automatically, but can be overridden
1.702+ with this spec.")
1.703+
1.704+(eplot-def (max number)
1.705+ "The maximum value in the chart.
1.706+This is normally computed automatically, but can be overridden
1.707+ with this spec.")
1.708+
1.709+(eplot-def (title string)
1.710+ "The title of the chart, if any.")
1.711+
1.712+(eplot-def (title-color string (spec chart-color))
1.713+ "The color of the title.")
1.714+
1.715+(eplot-def (x-title string)
1.716+ "The title of the X axis, if any.")
1.717+
1.718+(eplot-def (y-title string)
1.719+ "The title of the X axis, if any.")
1.720+
1.721+(eplot-def (x-label-format string)
1.722+ "Format string for the X labels.
1.723+This is a `format' string.")
1.724+
1.725+(eplot-def (y-label-format string)
1.726+ "Format string for the Y labels.
1.727+This is a `format' string.")
1.728+
1.729+(eplot-def (x-label-orientation symbol horizontal (horizontal vertical))
1.730+ "Orientation of the X labels.")
1.731+
1.732+(eplot-def (background-image-file string)
1.733+ "Use an image as the background.")
1.734+
1.735+(eplot-def (background-image-opacity number 1)
1.736+ "The opacity of the background image.")
1.737+
1.738+(eplot-def (background-image-cover symbol all (all plot frame))
1.739+ "Position of the background image.
1.740+Valid values are `all' (the entire image), `plot' (the plot area)
1.741+and `frame' (the surrounding area).")
1.742+
1.743+(eplot-def (header-file string)
1.744+ "File where the headers are.")
1.745+
1.746+(defvar eplot-compact-defaults
1.747+ '((margin-left 30)
1.748+ (margin-right 10)
1.749+ (margin-top 20)
1.750+ (margin-bottom 21)
1.751+ (font-size 12)
1.752+ (x-axis-title-space 3)))
1.753+
1.754+(defvar eplot-dark-defaults
1.755+ '((chart-color "#c0c0c0")
1.756+ (axes-color "#c0c0c0")
1.757+ (grid-color "#404040")
1.758+ (background-color "#101010")
1.759+ (label-color "#c0c0c0")
1.760+ (legend-color "#c0c0c0")
1.761+ (title-color "#c0c0c0")))
1.762+
1.763+(defvar eplot-bar-chart-defaults
1.764+ '((grid-position top)
1.765+ (grid y)
1.766+ (grid-opacity 0.2)
1.767+ (min 0)))
1.768+
1.769+(defvar eplot-horizontal-bar-chart-defaults
1.770+ '((grid-position top)
1.771+ (grid-opacity 0.2)
1.772+ (min 0)))
1.773+
1.774+(defclass eplot-chart ()
1.775+ (
1.776+ (plots :initarg :plots)
1.777+ (data :initarg :data)
1.778+ (xs)
1.779+ (ys)
1.780+ (x-values :initform nil)
1.781+ (x-type :initform nil)
1.782+ (x-min)
1.783+ (x-max)
1.784+ (x-ticks)
1.785+ (y-ticks)
1.786+ (y-labels)
1.787+ (x-labels)
1.788+ (print-format)
1.789+ (x-tick-step)
1.790+ (x-label-step)
1.791+ (x-step-map :initform nil)
1.792+ (y-tick-step)
1.793+ (y-label-step)
1.794+ (inhibit-compute-x-step :initform nil)
1.795+ ;; ---- CUT HERE ----
1.796+ (axes-color :initarg :axes-color :initform nil)
1.797+ (background-color :initarg :background-color :initform nil)
1.798+ (background-gradient :initarg :background-gradient :initform nil)
1.799+ (background-image-cover :initarg :background-image-cover :initform nil)
1.800+ (background-image-file :initarg :background-image-file :initform nil)
1.801+ (background-image-opacity :initarg :background-image-opacity :initform nil)
1.802+ (bar-font :initarg :bar-font :initform nil)
1.803+ (bar-font-size :initarg :bar-font-size :initform nil)
1.804+ (bar-font-weight :initarg :bar-font-weight :initform nil)
1.805+ (border-color :initarg :border-color :initform nil)
1.806+ (border-width :initarg :border-width :initform nil)
1.807+ (chart-color :initarg :chart-color :initform nil)
1.808+ (font :initarg :font :initform nil)
1.809+ (font-size :initarg :font-size :initform nil)
1.810+ (font-weight :initarg :font-weight :initform nil)
1.811+ (format :initarg :format :initform nil)
1.812+ (frame-color :initarg :frame-color :initform nil)
1.813+ (frame-width :initarg :frame-width :initform nil)
1.814+ (grid :initarg :grid :initform nil)
1.815+ (grid-color :initarg :grid-color :initform nil)
1.816+ (grid-opacity :initarg :grid-opacity :initform nil)
1.817+ (grid-position :initarg :grid-position :initform nil)
1.818+ (header-file :initarg :header-file :initform nil)
1.819+ (height :initarg :height :initform nil)
1.820+ (label-color :initarg :label-color :initform nil)
1.821+ (label-font :initarg :label-font :initform nil)
1.822+ (label-font-size :initarg :label-font-size :initform nil)
1.823+ (layout :initarg :layout :initform nil)
1.824+ (legend :initarg :legend :initform nil)
1.825+ (legend-background-color :initarg :legend-background-color :initform nil)
1.826+ (legend-border-color :initarg :legend-border-color :initform nil)
1.827+ (legend-color :initarg :legend-color :initform nil)
1.828+ (margin-bottom :initarg :margin-bottom :initform nil)
1.829+ (margin-left :initarg :margin-left :initform nil)
1.830+ (margin-right :initarg :margin-right :initform nil)
1.831+ (margin-top :initarg :margin-top :initform nil)
1.832+ (max :initarg :max :initform nil)
1.833+ (min :initarg :min :initform nil)
1.834+ (mode :initarg :mode :initform nil)
1.835+ (surround-color :initarg :surround-color :initform nil)
1.836+ (title :initarg :title :initform nil)
1.837+ (title-color :initarg :title-color :initform nil)
1.838+ (width :initarg :width :initform nil)
1.839+ (x-axis-title-space :initarg :x-axis-title-space :initform nil)
1.840+ (x-title :initarg :x-title :initform nil)
1.841+ (y-title :initarg :y-title :initform nil)
1.842+ (x-label-format :initarg :x-label-format :initform nil)
1.843+ (x-label-orientation :initarg :x-label-orientation :initform nil)
1.844+ (y-label-format :initarg :y-label-format :initform nil)
1.845+ ;; ---- CUT HERE ----
1.846+ ))
1.847+
1.848+;;; Parameters that are plot specific.
1.849+
1.850+(defmacro eplot-pdef (args doc-string)
1.851+ (declare (indent defun))
1.852+ `(eplot--pdef ',(nth 0 args) ',(nth 1 args) ',(nth 2 args) ',(nth 3 args)
1.853+ ,doc-string))
1.854+
1.855+(defun eplot--pdef (name type default valid doc)
1.856+ (setq eplot--plot-headers (delq (assq name eplot--plot-headers)
1.857+ eplot--plot-headers))
1.858+ (push (list name
1.859+ :type type
1.860+ :default default
1.861+ :valid valid
1.862+ :doc doc)
1.863+ eplot--plot-headers))
1.864+
1.865+(eplot-pdef (smoothing symbol nil (moving-average nil))
1.866+ "Smoothing algorithm to apply to the data, if any.
1.867+Valid values are `moving-average' and, er, probably more to come.")
1.868+
1.869+(eplot-pdef (gradient string)
1.870+ "Gradient to apply to the plot.
1.871+The syntax is:
1.872+
1.873+ from-color to-color direction position
1.874+
1.875+The last two parameters are optional.
1.876+
1.877+direction is either `top-down' (the default), `bottom-up',
1.878+`left-right' or `right-left').
1.879+
1.880+position is either `below' or `above'.
1.881+
1.882+to-color can be either a color name, or a string that defines
1.883+stops and colors:
1.884+
1.885+ Gradient: black 25-purple-50-white-75-purple-black
1.886+
1.887+In that case, the second element specifies the percentage points
1.888+of where each color ends, so the above starts with black, then at
1.889+25% it's purple, then at 50% it's white, then it's back to purple
1.890+again at 75%, before ending up at black at a 100% (but you don't
1.891+have to include the 100% here -- it's understood).")
1.892+
1.893+(eplot-pdef (style symbol line ( line impulse point square circle cross
1.894+ triangle rectangle curve))
1.895+ "Style the plot should be drawn in.
1.896+Valid values are listed below. Some styles take additional
1.897+optional parameters.
1.898+
1.899+line
1.900+ Straight lines between values.
1.901+
1.902+curve
1.903+ Curved lines between values.
1.904+
1.905+impulse
1.906+ size: width of the impulse
1.907+
1.908+point
1.909+
1.910+square
1.911+
1.912+circle
1.913+ size: diameter of the circle
1.914+ fill-color: color to fill the center
1.915+
1.916+cross
1.917+ size: length of the lines in the cross
1.918+
1.919+triangle
1.920+ size: length of the sides of the triangle
1.921+ fill-color: color to fill the center
1.922+
1.923+rectangle
1.924+ size: length of the sides of the rectangle
1.925+ fill-color: color to fill the center")
1.926+
1.927+(eplot-pdef (fill-color string)
1.928+ "Color to use to fill the plot styles that are closed shapes.
1.929+I.e., circle, triangle and rectangle.")
1.930+
1.931+(eplot-pdef (color string (spec chart-color))
1.932+ "Color to draw the plot.")
1.933+
1.934+(eplot-pdef (data-format symbol single (single date time xy))
1.935+ "Format of the data.
1.936+By default, eplot assumes that each line has a single data point.
1.937+This can also be `date', `time' and `xy'.
1.938+
1.939+date: The first column is a date on ISO8601 format (i.e., YYYYMMDD).
1.940+
1.941+time: The first column is a clock (i.e., HHMMSS).
1.942+
1.943+xy: The first column is the X position.")
1.944+
1.945+(eplot-pdef (data-column number 1)
1.946+ "Column where the data is.")
1.947+
1.948+(eplot-pdef (fill-border-color string)
1.949+ "Border around the fill area when using a fill/gradient style.")
1.950+
1.951+(eplot-pdef (size number)
1.952+ "Size of elements in styles that have meaningful sizes.")
1.953+
1.954+(eplot-pdef (size-factor number)
1.955+ "Multiply the size of the elements by the value.")
1.956+
1.957+(eplot-pdef (data-file string)
1.958+ "File where the data is.")
1.959+
1.960+(eplot-pdef (data-format symbol-list nil (nil two-values date time))
1.961+ "List of symbols to describe the data format.
1.962+Elements allowed are `two-values', `date' and `time'.")
1.963+
1.964+(eplot-pdef (name string)
1.965+ "Name of the plot, which will be displayed if legends are switched on.")
1.966+
1.967+(eplot-pdef (legend-color string (spec chart-color))
1.968+ "Color for the name to be displayed in the legend.")
1.969+
1.970+(eplot-pdef (bezier-factor number 0.1)
1.971+ "The Bezier factor to apply to curve plots.")
1.972+
1.973+(defclass eplot-plot ()
1.974+ (
1.975+ (values :initarg :values)
1.976+ ;; ---- CUT HERE ----
1.977+ (bezier-factor :initarg :bezier-factor :initform nil)
1.978+ (color :initarg :color :initform nil)
1.979+ (data-column :initarg :data-column :initform nil)
1.980+ (data-file :initarg :data-file :initform nil)
1.981+ (data-format :initarg :data-format :initform nil)
1.982+ (fill-border-color :initarg :fill-border-color :initform nil)
1.983+ (fill-color :initarg :fill-color :initform nil)
1.984+ (gradient :initarg :gradient :initform nil)
1.985+ (legend-color :initarg :legend-color :initform nil)
1.986+ (name :initarg :name :initform nil)
1.987+ (size :initarg :size :initform nil)
1.988+ (size-factor :initarg :size-factor :initform nil)
1.989+ (smoothing :initarg :smoothing :initform nil)
1.990+ (style :initarg :style :initform nil)
1.991+ ;; ---- CUT HERE ----
1.992+ ))
1.993+
1.994+(defun eplot--make-plot (data)
1.995+ "Make an `eplot-plot' object and initialize based on DATA."
1.996+ (let ((plot (make-instance 'eplot-plot
1.997+ :values (cdr (assq :values data)))))
1.998+ ;; Get the program-defined defaults.
1.999+ (eplot--object-defaults plot eplot--plot-headers)
1.1000+ ;; One special case. I don't think this hack is quite right...
1.1001+ (when (or (eq (eplot--vs 'mode data) 'dark)
1.1002+ (eq (cdr (assq 'mode eplot--user-defaults)) 'dark))
1.1003+ (setf (slot-value plot 'color) "#c0c0c0"))
1.1004+ ;; Use the headers.
1.1005+ (eplot--object-values plot (cdr (assq :headers data)) eplot--plot-headers)
1.1006+ plot))
1.1007+
1.1008+(defun eplot--make-chart (data)
1.1009+ "Make an `eplot-chart' object and initialize based on DATA."
1.1010+ (let ((chart (make-instance 'eplot-chart
1.1011+ :plots (mapcar #'eplot--make-plot
1.1012+ (eplot--vs :plots data))
1.1013+ :data data)))
1.1014+ ;; First get the program-defined defaults.
1.1015+ (eplot--object-defaults chart eplot--chart-headers)
1.1016+ ;; Then do the "meta" variables.
1.1017+ (eplot--meta chart data 'mode 'dark eplot-dark-defaults)
1.1018+ (eplot--meta chart data 'layout 'compact eplot-compact-defaults)
1.1019+ (eplot--meta chart data 'format 'bar-chart eplot-bar-chart-defaults)
1.1020+ (eplot--meta chart data 'format 'horizontal-bar-chart
1.1021+ eplot-horizontal-bar-chart-defaults)
1.1022+ ;; Set defaults from user settings/transients.
1.1023+ (cl-loop for (name . value) in eplot--user-defaults
1.1024+ when (assq name eplot--chart-headers)
1.1025+ do
1.1026+ (setf (slot-value chart name) value)
1.1027+ (eplot--set-dependent-values chart name value))
1.1028+ ;; Finally, use the data from the chart.
1.1029+ (eplot--object-values chart data eplot--chart-headers)
1.1030+ ;; If not set, recompute the margins based on the font sizes (if
1.1031+ ;; the font size has been changed from defaults).
1.1032+ (when (or (assq 'font-size eplot--user-defaults)
1.1033+ (assq 'font-size data))
1.1034+ (with-slots ( title x-title y-title
1.1035+ margin-top margin-bottom margin-left
1.1036+ font-size font font-weight)
1.1037+ chart
1.1038+ (when (or title x-title y-title)
1.1039+ (let ((text-height
1.1040+ (eplot--text-height (concat title x-title y-title)
1.1041+ font font-size font-weight)))
1.1042+ (when (and title
1.1043+ (and (not (assq 'margin-top eplot--user-defaults))
1.1044+ (not (assq 'margin-top data))))
1.1045+ (cl-incf margin-top (* text-height 1.4)))
1.1046+ (when (and x-title
1.1047+ (and (not (assq 'margin-bottom eplot--user-defaults))
1.1048+ (not (assq 'margin-bottom data))))
1.1049+ (cl-incf margin-bottom (* text-height 1.4)))
1.1050+ (when (and y-title
1.1051+ (and (not (assq 'margin-left eplot--user-defaults))
1.1052+ (not (assq 'margin-left data))))
1.1053+ (cl-incf margin-left (* text-height 1.4)))))))
1.1054+ chart))
1.1055+
1.1056+(defun eplot--meta (chart data slot value defaults)
1.1057+ (when (or (eq (cdr (assq slot eplot--user-defaults)) value)
1.1058+ (eq (eplot--vy slot data) value))
1.1059+ (eplot--set-theme chart defaults)))
1.1060+
1.1061+(defun eplot--object-defaults (object headers)
1.1062+ (dolist (header headers)
1.1063+ (when-let ((default (plist-get (cdr header) :default)))
1.1064+ (setf (slot-value object (car header))
1.1065+ ;; Allow overrides via `eplot-set'.
1.1066+ (or (cdr (assq (car header) eplot--user-defaults))
1.1067+ (if (and (consp default)
1.1068+ (eq (car default) 'spec))
1.1069+ ;; Chase dependencies.
1.1070+ (eplot--default (cadr default))
1.1071+ default))))))
1.1072+
1.1073+(defun eplot--object-values (object data headers)
1.1074+ (cl-loop for (name . value) in data
1.1075+ do (unless (eq name :plots)
1.1076+ (let ((spec (cdr (assq name headers))))
1.1077+ (if (not spec)
1.1078+ (error "%s is not a valid spec" name)
1.1079+ (let ((value
1.1080+ (cl-case (plist-get spec :type)
1.1081+ (number
1.1082+ (string-to-number value))
1.1083+ (symbol
1.1084+ (intern (downcase value)))
1.1085+ (symbol-list
1.1086+ (mapcar #'intern (split-string (downcase value))))
1.1087+ (t
1.1088+ value))))
1.1089+ (setf (slot-value object name) value)
1.1090+ (eplot--set-dependent-values object name value)))))))
1.1091+
1.1092+(defun eplot--set-dependent-values (object name value)
1.1093+ (dolist (slot (gethash name (eplot--dependecy-graph)))
1.1094+ (setf (slot-value object slot) value)
1.1095+ (eplot--set-dependent-values object slot value)))
1.1096+
1.1097+(defun eplot--set-theme (chart map)
1.1098+ (cl-loop for (slot value) in map
1.1099+ do (setf (slot-value chart slot) value)))
1.1100+
1.1101+(defun eplot--default (slot)
1.1102+ "Find the default value for SLOT, chasing dependencies."
1.1103+ (let ((spec (cdr (assq slot eplot--chart-headers))))
1.1104+ (unless spec
1.1105+ (error "Invalid slot %s" slot))
1.1106+ (let ((default (plist-get spec :default)))
1.1107+ (if (and (consp default)
1.1108+ (eq (car default) 'spec))
1.1109+ (eplot--default (cadr default))
1.1110+ (or (cdr (assq slot eplot--user-defaults)) default)))))
1.1111+
1.1112+(defun eplot--dependecy-graph ()
1.1113+ (let ((table (make-hash-table)))
1.1114+ (dolist (elem eplot--chart-headers)
1.1115+ (let ((default (plist-get (cdr elem) :default)))
1.1116+ (when (and (consp default)
1.1117+ (eq (car default) 'spec))
1.1118+ (push (car elem) (gethash (cadr default) table)))))
1.1119+ table))
1.1120+
1.1121+(defun eplot--render (data &optional return-image)
1.1122+ "Create the chart and display it.
1.1123+If RETURN-IMAGE is non-nil, return it instead of displaying it."
1.1124+ (let* ((chart (eplot--make-chart data))
1.1125+ svg)
1.1126+ (with-slots ( width height xs ys
1.1127+ margin-left margin-right margin-top margin-bottom
1.1128+ grid-position plots x-min format
1.1129+ x-label-orientation)
1.1130+ chart
1.1131+ ;; Set the size of the chart based on the window it's going to
1.1132+ ;; be displayed in. It uses the *eplot* window by default, or
1.1133+ ;; the current one if that isn't displayed.
1.1134+ (let ((factor (image-compute-scaling-factor image-scaling-factor)))
1.1135+ (unless width
1.1136+ (setq width (truncate
1.1137+ (/ (* (window-pixel-width
1.1138+ (get-buffer-window "*eplot*" t))
1.1139+ 0.9)
1.1140+ factor))))
1.1141+ (unless height
1.1142+ (setq height (truncate
1.1143+ (/ (* (window-pixel-height
1.1144+ (get-buffer-window "*eplot*" t))
1.1145+ 0.9)
1.1146+ factor)))))
1.1147+ (setq svg (svg-create width height)
1.1148+ xs (- width margin-left margin-right)
1.1149+ ys (- height margin-top margin-bottom))
1.1150+ ;; Protect against being called in an empty buffer.
1.1151+ (if (not (and plots
1.1152+ ;; Sanity check against the user choosing dimensions
1.1153+ ;; that leave no space for the plot.
1.1154+ (> ys 0) (> xs 0)))
1.1155+ ;; Just draw the basics.
1.1156+ (eplot--draw-basics svg chart)
1.1157+
1.1158+ ;; Horizontal bar charts are special.
1.1159+ (when (eq format 'horizontal-bar-chart)
1.1160+ (eplot--adjust-horizontal-bar-chart chart data))
1.1161+ ;; Compute min/max based on all plots, and also compute x-ticks
1.1162+ ;; etc.
1.1163+ (eplot--compute-chart-dimensions chart)
1.1164+ (when (and (eq x-label-orientation 'vertical)
1.1165+ (eplot--default-p 'margin-bottom (slot-value chart 'data)))
1.1166+ (eplot--adjust-vertical-x-labels chart))
1.1167+ ;; Analyze values and adjust values accordingly.
1.1168+ (eplot--adjust-chart chart)
1.1169+ ;; Compute the Y labels -- this may adjust `margin-left'.
1.1170+ (eplot--compute-y-labels chart)
1.1171+ ;; Compute the X labels -- this may adjust `margin-bottom'.
1.1172+ (eplot--compute-x-labels chart)
1.1173+ ;; Draw background/borders/titles/etc.
1.1174+ (eplot--draw-basics svg chart)
1.1175+
1.1176+ (when (eq grid-position 'top)
1.1177+ (eplot--draw-plots svg chart))
1.1178+
1.1179+ (eplot--draw-x-ticks svg chart)
1.1180+ (unless (eq format 'horizontal-bar-chart)
1.1181+ (eplot--draw-y-ticks svg chart))
1.1182+
1.1183+ ;; Draw axes.
1.1184+ (with-slots ( margin-left margin-right margin-margin-top
1.1185+ margin-bottom axes-color)
1.1186+ chart
1.1187+ (svg-line svg margin-left margin-top margin-left
1.1188+ (+ (- height margin-bottom) 5)
1.1189+ :stroke axes-color)
1.1190+ (svg-line svg (- margin-left 5) (- height margin-bottom)
1.1191+ (- width margin-right) (- height margin-bottom)
1.1192+ :stroke axes-color))
1.1193+
1.1194+ (when (eq grid-position 'bottom)
1.1195+ (eplot--draw-plots svg chart)))
1.1196+
1.1197+ (with-slots (frame-color frame-width) chart
1.1198+ (when (or frame-color frame-width)
1.1199+ (svg-rectangle svg margin-left margin-top xs ys
1.1200+ :stroke-width frame-width
1.1201+ :fill "none"
1.1202+ :stroke-color frame-color)))
1.1203+ (eplot--draw-legend svg chart))
1.1204+
1.1205+ (if return-image
1.1206+ svg
1.1207+ (svg-insert-image svg)
1.1208+ chart)))
1.1209+
1.1210+(defun eplot--adjust-horizontal-bar-chart (chart data)
1.1211+ (with-slots ( plots bar-font bar-font-size bar-font-weight margin-left
1.1212+ width margin-right xs)
1.1213+ chart
1.1214+ (with-slots ( data-format values) (car plots)
1.1215+ (push 'xy data-format)
1.1216+ ;; Flip the values -- we want the values to be on the X
1.1217+ ;; axis instead.
1.1218+ (setf values
1.1219+ (cl-loop for value in values
1.1220+ for i from 1
1.1221+ collect (list :value i
1.1222+ :x (plist-get value :value)
1.1223+ :settings
1.1224+ (plist-get value :settings))))
1.1225+ (when (eplot--default-p 'margin-left data)
1.1226+ (setf margin-left
1.1227+ (+ (cl-loop for value in values
1.1228+ maximize
1.1229+ (eplot--text-width
1.1230+ (eplot--vs 'label (plist-get value :settings))
1.1231+ bar-font bar-font-size bar-font-weight))
1.1232+ 20)
1.1233+ xs (- width margin-left margin-right))))))
1.1234+
1.1235+(defun eplot--draw-basics (svg chart)
1.1236+ (with-slots ( width height
1.1237+ chart-color font font-size font-weight
1.1238+ margin-left margin-right margin-top margin-bottom
1.1239+ background-color label-color
1.1240+ xs ys)
1.1241+ chart
1.1242+ ;; Add background.
1.1243+ (eplot--draw-background chart svg 0 0 width height)
1.1244+ (with-slots ( background-image-file background-image-opacity
1.1245+ background-image-cover)
1.1246+ chart
1.1247+ (when (and background-image-file
1.1248+ ;; Sanity checks to avoid erroring out later.
1.1249+ (file-exists-p background-image-file)
1.1250+ (file-regular-p background-image-file))
1.1251+ (apply #'svg-embed svg background-image-file "image/jpeg" nil
1.1252+ :opacity background-image-opacity
1.1253+ :preserveAspectRatio "xMidYMid slice"
1.1254+ (if (memq background-image-cover '(all frame))
1.1255+ `(:x 0 :y 0 :width ,width :height ,height)
1.1256+ `(:x ,margin-left :y ,margin-top :width ,xs :height ,ys)))
1.1257+ (when (eq background-image-cover 'frame)
1.1258+ (eplot--draw-background chart svg margin-left margin-right xs ys))))
1.1259+ ;; Area between plot and edges.
1.1260+ (with-slots (surround-color) chart
1.1261+ (when surround-color
1.1262+ (svg-rectangle svg 0 0 width height
1.1263+ :fill surround-color)
1.1264+ (svg-rectangle svg margin-left margin-top
1.1265+ xs ys
1.1266+ :fill background-color)))
1.1267+ ;; Border around the entire chart.
1.1268+ (with-slots (border-width border-color) chart
1.1269+ (when (or border-width border-color)
1.1270+ (svg-rectangle svg 0 0 width height
1.1271+ :stroke-width (or border-width 1)
1.1272+ :fill "none"
1.1273+ :stroke-color (or border-color chart-color))))
1.1274+ ;; Frame around the plot.
1.1275+ (with-slots (frame-width frame-color) chart
1.1276+ (when (or frame-width frame-color)
1.1277+ (svg-rectangle svg margin-left margin-top xs ys
1.1278+ :stroke-width (or frame-width 1)
1.1279+ :fill "none"
1.1280+ :stroke-color (or frame-color chart-color))))
1.1281+ ;; Title and legends.
1.1282+ (with-slots (title title-color) chart
1.1283+ (when title
1.1284+ (svg-text svg title
1.1285+ :font-family font
1.1286+ :text-anchor "middle"
1.1287+ :font-weight font-weight
1.1288+ :font-size font-size
1.1289+ :fill title-color
1.1290+ :x (+ margin-left (/ (- width margin-left margin-right) 2))
1.1291+ :y (+ 3 (/ margin-top 2)))))
1.1292+ (with-slots (x-title) chart
1.1293+ (when x-title
1.1294+ (svg-text svg x-title
1.1295+ :font-family font
1.1296+ :text-anchor "middle"
1.1297+ :font-weight font-weight
1.1298+ :font-size font-size
1.1299+ :fill label-color
1.1300+ :x (+ margin-left (/ (- width margin-left margin-right) 2))
1.1301+ :y (- height (/ margin-bottom 4)))))
1.1302+ (with-slots (y-title) chart
1.1303+ (when y-title
1.1304+ (let ((text-height
1.1305+ (eplot--text-height y-title font font-size font-weight)))
1.1306+ (svg-text svg y-title
1.1307+ :font-family font
1.1308+ :text-anchor "middle"
1.1309+ :font-weight font-weight
1.1310+ :font-size font-size
1.1311+ :fill label-color
1.1312+ :transform
1.1313+ (format "translate(%s,%s) rotate(-90)"
1.1314+ (- (/ margin-left 2) (/ text-height 2) 4)
1.1315+ (+ margin-top
1.1316+ (/ (- height margin-bottom margin-top) 2)))))))))
1.1317+
1.1318+(defun eplot--draw-background (chart svg left top width height)
1.1319+ (with-slots (background-gradient background-color) chart
1.1320+ (let ((gradient (eplot--parse-gradient background-gradient))
1.1321+ id)
1.1322+ (when gradient
1.1323+ (setq id (format "gradient-%s" (make-temp-name "grad")))
1.1324+ (eplot--gradient svg id 'linear
1.1325+ (eplot--stops (eplot--vs 'from gradient)
1.1326+ (eplot--vs 'to gradient))
1.1327+ (eplot--vs 'direction gradient)))
1.1328+ (apply #'svg-rectangle svg left top width height
1.1329+ (if gradient
1.1330+ `(:gradient ,id)
1.1331+ `(:fill ,background-color))))))
1.1332+
1.1333+(defun eplot--compute-chart-dimensions (chart)
1.1334+ (with-slots ( min max plots x-values x-min x-max x-ticks
1.1335+ print-format font-size
1.1336+ xs
1.1337+ inhibit-compute-x-step x-type x-step-map format
1.1338+ x-tick-step x-label-step
1.1339+ label-font label-font-size x-label-format)
1.1340+ chart
1.1341+ (let ((set-min min)
1.1342+ (set-max max))
1.1343+ (dolist (plot plots)
1.1344+ (with-slots (values data-format) plot
1.1345+ (let* ((vals (nconc (seq-map (lambda (v) (plist-get v :value)) values)
1.1346+ (and (memq 'two-values data-format)
1.1347+ (seq-map
1.1348+ (lambda (v) (plist-get v :extra-value))
1.1349+ values)))))
1.1350+ ;; Set the x-values based on the first plot.
1.1351+ (unless x-values
1.1352+ (setq print-format (cond
1.1353+ ((memq 'year data-format) 'year)
1.1354+ ((memq 'date data-format) 'date)
1.1355+ ((memq 'time data-format) 'time)
1.1356+ (t 'number)))
1.1357+ (cond
1.1358+ ((or (memq 'xy data-format)
1.1359+ (memq 'year data-format))
1.1360+ (setq x-values (cl-loop for val in values
1.1361+ collect (plist-get val :x))
1.1362+ x-min (if (eq format 'horizontal-bar-chart)
1.1363+ 0
1.1364+ (seq-min x-values))
1.1365+ x-max (seq-max x-values)
1.1366+ x-ticks (eplot--get-ticks x-min x-max xs))
1.1367+ (when (memq 'year data-format)
1.1368+ (setq print-format 'literal-year)))
1.1369+ ((memq 'date data-format)
1.1370+ (setq x-values
1.1371+ (cl-loop for val in values
1.1372+ collect
1.1373+ (time-to-days
1.1374+ (encode-time
1.1375+ (decoded-time-set-defaults
1.1376+ (iso8601-parse-date
1.1377+ (format "%d" (plist-get val :x)))))))
1.1378+ x-min (seq-min x-values)
1.1379+ x-max (seq-max x-values)
1.1380+ inhibit-compute-x-step t)
1.1381+ (let ((xs (eplot--get-date-ticks
1.1382+ x-min x-max xs
1.1383+ label-font label-font-size x-label-format)))
1.1384+ (setq x-ticks (car xs)
1.1385+ print-format (cadr xs)
1.1386+ x-tick-step 1
1.1387+ x-label-step 1
1.1388+ x-step-map (nth 2 xs))))
1.1389+ ((memq 'time data-format)
1.1390+ (setq x-values
1.1391+ (cl-loop for val in values
1.1392+ collect
1.1393+ (time-convert
1.1394+ (encode-time
1.1395+ (decoded-time-set-defaults
1.1396+ (iso8601-parse-time
1.1397+ (format "%06d" (plist-get val :x)))))
1.1398+ 'integer))
1.1399+ x-min (car x-values)
1.1400+ x-max (car (last x-values))
1.1401+ inhibit-compute-x-step t)
1.1402+ (let ((xs (eplot--get-time-ticks
1.1403+ x-min x-max xs label-font label-font-size
1.1404+ x-label-format)))
1.1405+ (setq x-ticks (car xs)
1.1406+ print-format (cadr xs)
1.1407+ x-tick-step 1
1.1408+ x-label-step 1
1.1409+ x-step-map (nth 2 xs))))
1.1410+ (t
1.1411+ ;; This is a one-dimensional plot -- we don't have X
1.1412+ ;; values, really, so we just do zero to (1- (length
1.1413+ ;; values)).
1.1414+ (setq x-type 'one-dimensional
1.1415+ x-values (cl-loop for i from 0
1.1416+ repeat (length values)
1.1417+ collect i)
1.1418+ x-min (car x-values)
1.1419+ x-max (car (last x-values))
1.1420+ x-ticks x-values))))
1.1421+ (unless set-min
1.1422+ (setq min (min (or min 1.0e+INF) (seq-min vals))))
1.1423+ (unless set-max
1.1424+ (setq max (max (or max -1.0e+INF) (seq-max vals))))))))))
1.1425+
1.1426+(defun eplot--adjust-chart (chart)
1.1427+ (with-slots ( x-tick-step x-label-step y-tick-step y-label-step
1.1428+ min max ys format inhibit-compute-x-step
1.1429+ y-ticks xs x-values print-format
1.1430+ x-label-format label-font label-font-size data
1.1431+ x-ticks)
1.1432+ chart
1.1433+ (setq y-ticks (and max
1.1434+ (eplot--get-ticks
1.1435+ min
1.1436+ ;; We get 5% more ticks to check whether we
1.1437+ ;; should extend max.
1.1438+ (if (eplot--default-p 'max data)
1.1439+ (* max 1.02)
1.1440+ max)
1.1441+ ys)))
1.1442+ (when (eplot--default-p 'max data)
1.1443+ (setq max (max max (car (last y-ticks)))))
1.1444+ (if (eq format 'bar-chart)
1.1445+ (setq x-tick-step 1
1.1446+ x-label-step 1)
1.1447+ (unless inhibit-compute-x-step
1.1448+ (let ((xt (eplot--compute-x-ticks
1.1449+ xs x-ticks print-format
1.1450+ x-label-format label-font label-font-size)))
1.1451+ (setq x-tick-step (car xt)
1.1452+ x-label-step (cadr xt)))))
1.1453+ (when max
1.1454+ (let ((yt (eplot--compute-y-ticks
1.1455+ ys y-ticks
1.1456+ (eplot--text-height "100" label-font label-font-size))))
1.1457+ (setq y-tick-step (car yt)
1.1458+ y-label-step (cadr yt))))
1.1459+ ;; If max is less than 2% off from a pleasant number, then
1.1460+ ;; increase max.
1.1461+ (when (eplot--default-p 'max data)
1.1462+ (cl-loop for tick in (reverse y-ticks)
1.1463+ when (and (< max tick)
1.1464+ (< (e/ (- tick max) (- max min)) 0.02))
1.1465+ return (progn
1.1466+ (setq max tick)
1.1467+ ;; Chop off any further ticks.
1.1468+ (setcdr (member tick y-ticks) nil))))
1.1469+
1.1470+ (when y-ticks
1.1471+ (if (and (eplot--default-p 'min data)
1.1472+ (< (car y-ticks) min))
1.1473+ (setq min (car y-ticks))
1.1474+ ;; We may be extending the bottom of the chart to get pleasing
1.1475+ ;; numbers. We don't want to be drawing the chart on top of the
1.1476+ ;; X axis, because the chart won't be visible there.
1.1477+ (when (and nil
1.1478+ (<= min (car y-ticks))
1.1479+ ;; But not if we start at origo, because that just
1.1480+ ;; looks confusing.
1.1481+ (not (zerop min)))
1.1482+ (setq min (- (car y-ticks)
1.1483+ ;; 2% of the value range.
1.1484+ (* 0.02 (- (car (last y-ticks)) (car y-ticks))))))))))
1.1485+
1.1486+(defun eplot--adjust-vertical-x-labels (chart)
1.1487+ (with-slots ( x-step-map x-ticks format plots
1.1488+ print-format x-label-format label-font
1.1489+ label-font-size margin-bottom
1.1490+ bar-font bar-font-size bar-font-weight)
1.1491+ chart
1.1492+ ;; Make X ticks.
1.1493+ (let ((width
1.1494+ (cl-loop
1.1495+ for xv in (or x-step-map x-ticks)
1.1496+ for x = (if (consp xv) (car xv) xv)
1.1497+ for i from 0
1.1498+ for value = (and (equal format 'bar-chart)
1.1499+ (elt (slot-value (car plots) 'values) i))
1.1500+ for label = (if (equal format 'bar-chart)
1.1501+ (eplot--vs 'label
1.1502+ (plist-get value :settings)
1.1503+ ;; When we're doing bar charts, we
1.1504+ ;; want default labeling to start with
1.1505+ ;; 1 and not zero.
1.1506+ (format "%s" (1+ x)))
1.1507+ (eplot--format-value x print-format x-label-format))
1.1508+ maximize (if (equal format 'bar-chart)
1.1509+ (eplot--text-width
1.1510+ label bar-font bar-font-size bar-font-weight)
1.1511+ (eplot--text-width
1.1512+ label label-font label-font-size)))))
1.1513+ ;; Ensure that we have enough room to display the X labels
1.1514+ ;; (unless overridden).
1.1515+ (with-slots ( height margin-top ys
1.1516+ y-ticks y-tick-step y-label-step min max)
1.1517+ chart
1.1518+ (setq margin-bottom (max margin-bottom (+ width 40))
1.1519+ ys (- height margin-top margin-bottom))))))
1.1520+
1.1521+(defun eplot--compute-x-labels (chart)
1.1522+ (with-slots ( x-step-map x-ticks
1.1523+ format plots print-format x-label-format x-labels
1.1524+ x-tick-step x-label-step
1.1525+ x-label-orientation margin-bottom)
1.1526+ chart
1.1527+ ;; Make X ticks.
1.1528+ (setf x-labels
1.1529+ (cl-loop
1.1530+ for xv in (or x-step-map x-ticks)
1.1531+ for x = (if (consp xv) (car xv) xv)
1.1532+ for do-tick = (if (consp xv)
1.1533+ (nth 1 xv)
1.1534+ (zerop (e% x x-tick-step)))
1.1535+ for do-label = (if (consp xv)
1.1536+ (nth 2 xv)
1.1537+ (zerop (e% x x-label-step)))
1.1538+ for i from 0
1.1539+ for value = (and (equal format 'bar-chart)
1.1540+ (elt (slot-value (car plots) 'values) i))
1.1541+ collect (list
1.1542+ (if (equal format 'bar-chart)
1.1543+ (eplot--vs 'label
1.1544+ (plist-get value :settings)
1.1545+ ;; When we're doing bar charts, we
1.1546+ ;; want default labeling to start with
1.1547+ ;; 1 and not zero.
1.1548+ (format "%s" (1+ x)))
1.1549+ (eplot--format-value x print-format x-label-format))
1.1550+ do-tick
1.1551+ do-label)))))
1.1552+
1.1553+(defun eplot--draw-x-ticks (svg chart)
1.1554+ (with-slots ( x-step-map x-ticks format layout print-format
1.1555+ margin-left margin-right margin-top margin-bottom
1.1556+ x-min x-max xs
1.1557+ width height
1.1558+ axes-color label-color
1.1559+ grid grid-opacity grid-color
1.1560+ font x-tick-step x-label-step x-label-format x-label-orientation
1.1561+ label-font label-font-size
1.1562+ plots x-labels
1.1563+ bar-font bar-font-size bar-font-weight)
1.1564+ chart
1.1565+ (let ((font label-font)
1.1566+ (font-size label-font-size)
1.1567+ (font-weight 'normal))
1.1568+ (when (equal format 'bar-chart)
1.1569+ (setq font bar-font
1.1570+ font-size bar-font-size
1.1571+ font-weight bar-font-weight))
1.1572+ ;; Make X ticks.
1.1573+ (cl-loop with label-height
1.1574+ for xv in (or x-step-map x-ticks)
1.1575+ for x = (if (consp xv) (car xv) xv)
1.1576+ for i from 0
1.1577+ for (label do-tick do-label) in x-labels
1.1578+ for stride = (eplot--stride chart x-ticks)
1.1579+ for px = (if (equal format 'bar-chart)
1.1580+ (+ margin-left (* x stride) (/ stride 2)
1.1581+ (/ (* stride 0.1) 2))
1.1582+ (+ margin-left
1.1583+ (* (/ (- (* 1.0 x) x-min) (- x-max x-min))
1.1584+ xs)))
1.1585+ ;; We might have one extra stride outside the area -- don't
1.1586+ ;; draw it.
1.1587+ when (<= px (- width margin-right))
1.1588+ do
1.1589+ (when do-tick
1.1590+ ;; Draw little tick.
1.1591+ (unless (equal format 'bar-chart)
1.1592+ (svg-line svg
1.1593+ px (- height margin-bottom)
1.1594+ px (+ (- height margin-bottom)
1.1595+ (if do-label
1.1596+ 4
1.1597+ 2))
1.1598+ :stroke axes-color))
1.1599+ (when (or (eq grid 'xy) (eq grid 'x))
1.1600+ (svg-line svg px margin-top
1.1601+ px (- height margin-bottom)
1.1602+ :opacity grid-opacity
1.1603+ :stroke grid-color)))
1.1604+ (when (and do-label
1.1605+ ;; We want to skip marking the first X value
1.1606+ ;; unless we're a bar chart or we're a one
1.1607+ ;; dimensional chart.
1.1608+ (or (equal format 'bar-chart)
1.1609+ t
1.1610+ (not (= x-min (car x-values)))
1.1611+ (eq x-type 'one-dimensional)
1.1612+ (and (not (zerop x)) (not (zerop i)))))
1.1613+ (if (eq x-label-orientation 'vertical)
1.1614+ (progn
1.1615+ (unless label-height
1.1616+ ;; The X position we're putting the label at is
1.1617+ ;; based on the bottom of the lower-case
1.1618+ ;; characters. So we want to ignore descenders
1.1619+ ;; etc, so we use "xx" to determine the height
1.1620+ ;; to be able to center the text.
1.1621+ (setq label-height
1.1622+ (eplot--text-height
1.1623+ ;; If the labels are numerical, we need
1.1624+ ;; to center them using the height of
1.1625+ ;; numbers.
1.1626+ (if (string-match "^[0-9]+$" label)
1.1627+ "10"
1.1628+ ;; Otherwise center them on the baseline.
1.1629+ "xx")
1.1630+ font font-size font-weight)))
1.1631+ (svg-text svg label
1.1632+ :font-family font
1.1633+ :text-anchor "end"
1.1634+ :font-size font-size
1.1635+ :font-weight font-weight
1.1636+ :fill label-color
1.1637+ :transform
1.1638+ (format "translate(%s,%s) rotate(-90)"
1.1639+ (+ px (/ label-height 2))
1.1640+ (- height margin-bottom -10))))
1.1641+ (svg-text svg label
1.1642+ :font-family font
1.1643+ :text-anchor "middle"
1.1644+ :font-size font-size
1.1645+ :font-weight font-weight
1.1646+ :fill label-color
1.1647+ :x px
1.1648+ :y (+ (- height margin-bottom)
1.1649+ font-size
1.1650+ (if (equal format 'bar-chart)
1.1651+ (if (equal layout 'compact) 3 5)
1.1652+ 2)))))))))
1.1653+
1.1654+(defun eplot--stride (chart values)
1.1655+ (with-slots (xs x-type format) chart
1.1656+ (if (eq x-type 'one-dimensional)
1.1657+ (e/ xs
1.1658+ ;; Fenceposting bar-chart vs everything else.
1.1659+ (if (eq format 'bar-chart)
1.1660+ (length values)
1.1661+ (1- (length values))))
1.1662+ (e/ xs (length values)))))
1.1663+
1.1664+(defun eplot--default-p (slot data)
1.1665+ "Return non-nil if SLOT is at the default value."
1.1666+ (and (not (assq slot eplot--user-defaults))
1.1667+ (not (assq slot data))))
1.1668+
1.1669+(defun eplot--compute-y-labels (chart)
1.1670+ (with-slots ( y-ticks y-labels
1.1671+ width height min max xs ys
1.1672+ margin-top margin-bottom margin-left margin-right
1.1673+ y-tick-step y-label-step y-label-format)
1.1674+ chart
1.1675+ ;; First collect all the labels we're thinking about outputting.
1.1676+ (setq y-labels
1.1677+ (cl-loop for y in y-ticks
1.1678+ for py = (- (- height margin-bottom)
1.1679+ (* (/ (- (* 1.0 y) min) (- max min))
1.1680+ ys))
1.1681+ when (and (<= margin-top py (- height margin-bottom))
1.1682+ (zerop (e% y y-tick-step))
1.1683+ (zerop (e% y y-label-step)))
1.1684+ collect (eplot--format-y
1.1685+ y (- (cadr y-ticks) (car y-ticks)) nil
1.1686+ y-label-format)))
1.1687+ ;; Check the labels to see whether we have too many digits for
1.1688+ ;; what we're actually going to display. Man, this is a lot of
1.1689+ ;; back-and-forth and should be rewritten to be less insanely
1.1690+ ;; inefficient.
1.1691+ (when (= (seq-count (lambda (label)
1.1692+ (string-match "\\." label))
1.1693+ y-labels)
1.1694+ (length y-labels))
1.1695+ (setq y-labels
1.1696+ (cl-loop with max = (cl-loop for label in y-labels
1.1697+ maximize (eplot--decimal-digits
1.1698+ (string-to-number label)))
1.1699+ for label in y-labels
1.1700+ collect (format (if (zerop max)
1.1701+ "%d"
1.1702+ (format "%%.%df" max))
1.1703+ (string-to-number label)))))
1.1704+ (setq y-labels (cl-coerce y-labels 'vector))
1.1705+ ;; Ensure that we have enough room to display the Y labels
1.1706+ ;; (unless overridden).
1.1707+ (when (eplot--default-p 'margin-left (slot-value chart 'data))
1.1708+ (with-slots (label-font label-font-size) chart
1.1709+ (setq margin-left (max margin-left
1.1710+ (+ (eplot--text-width
1.1711+ (elt y-labels (1- (length y-labels)))
1.1712+ label-font label-font-size)
1.1713+ 10))
1.1714+ xs (- width margin-left margin-right))))))
1.1715+
1.1716+(defun eplot--draw-y-ticks (svg chart)
1.1717+ (with-slots ( y-ticks y-labels y-tick-step y-label-step label-color
1.1718+ label-font label-font-size
1.1719+ width height min max ys
1.1720+ margin-top margin-bottom margin-left margin-right
1.1721+ axes-color
1.1722+ grid grid-opacity grid-color)
1.1723+ chart
1.1724+ ;; Make Y ticks.
1.1725+ (cl-loop with lnum = 0
1.1726+ with text-height = (eplot--text-height
1.1727+ "012" label-font label-font-size)
1.1728+ for y in y-ticks
1.1729+ for i from 0
1.1730+ for py = (- (- height margin-bottom)
1.1731+ (* (/ (- (* 1.0 y) min) (- max min))
1.1732+ ys))
1.1733+ do
1.1734+ (when (and (<= margin-top py (- height margin-bottom))
1.1735+ (zerop (e% y y-tick-step)))
1.1736+ (svg-line svg margin-left py
1.1737+ (- margin-left 3) py
1.1738+ :stroke-color axes-color)
1.1739+ (when (or (eq grid 'xy) (eq grid 'y))
1.1740+ (svg-line svg margin-left py
1.1741+ (- width margin-right) py
1.1742+ :opacity grid-opacity
1.1743+ :stroke-color grid-color))
1.1744+ (when (zerop (e% y y-label-step))
1.1745+ (svg-text svg (elt y-labels lnum)
1.1746+ :font-family label-font
1.1747+ :text-anchor "end"
1.1748+ :font-size label-font-size
1.1749+ :fill label-color
1.1750+ :x (- margin-left 6)
1.1751+ :y (+ py (/ text-height 2) -1))
1.1752+ (cl-incf lnum))))))
1.1753+
1.1754+(defun eplot--text-width (text font font-size &optional font-weight)
1.1755+ (string-pixel-width
1.1756+ (propertize text 'face
1.1757+ (list :font (font-spec :family font
1.1758+ :weight (or font-weight 'normal)
1.1759+ :size font-size)))))
1.1760+
1.1761+(defvar eplot--text-size-cache (make-hash-table :test #'equal))
1.1762+
1.1763+(defun eplot--text-height (text font font-size &optional font-weight)
1.1764+ (cdr (eplot--text-size text font font-size font-weight)))
1.1765+
1.1766+(defun eplot--text-size (text font font-size font-weight)
1.1767+ (let ((key (list text font font-size font-weight)))
1.1768+ (or (gethash key eplot--text-size-cache)
1.1769+ (let ((size (eplot--text-size-1 text font font-size font-weight)))
1.1770+ (setf (gethash key eplot--text-size-cache) size)
1.1771+ size))))
1.1772+
1.1773+(defun eplot--text-size-1 (text font font-size font-weight)
1.1774+ (if (not (executable-find "convert"))
1.1775+ ;; This "default" text size is kinda bogus.
1.1776+ (cons (* (length text) font-size) font-size)
1.1777+ (let* ((size (* font-size 10))
1.1778+ (svg (svg-create size size))
1.1779+ text-size)
1.1780+ (svg-rectangle svg 0 0 size size :fill "black")
1.1781+ (svg-text svg text
1.1782+ :font-family font
1.1783+ :text-anchor "middle"
1.1784+ :font-size font-size
1.1785+ :font-weight (or font-weight 'normal)
1.1786+ :fill "white"
1.1787+ :x (/ size 2)
1.1788+ :y (/ size 2))
1.1789+ (with-temp-buffer
1.1790+ (set-buffer-multibyte nil)
1.1791+ (svg-print svg)
1.1792+ (let* ((file (concat (make-temp-name "/tmp/eplot") ".svg"))
1.1793+ (png (file-name-with-extension file ".png")))
1.1794+ (unwind-protect
1.1795+ (progn
1.1796+ (write-region (point-min) (point-max) file nil 'silent)
1.1797+ ;; rsvg-convert is 5x faster than convert when doing SVG, so
1.1798+ ;; if we have it, we use it.
1.1799+ (when (executable-find "rsvg-convert")
1.1800+ (unwind-protect
1.1801+ (call-process "rsvg-convert" nil nil nil
1.1802+ (format "--output=%s" png) file)
1.1803+ (when (file-exists-p png)
1.1804+ (delete-file file)
1.1805+ (setq file png))))
1.1806+ (erase-buffer)
1.1807+ (when (zerop (call-process "convert" nil t nil
1.1808+ "-trim" "+repage" file "info:-"))
1.1809+ (goto-char (point-min))
1.1810+ (when (re-search-forward " \\([0-9]+\\)x\\([0-9]+\\)" nil t)
1.1811+ (setq text-size
1.1812+ (cons (string-to-number (match-string 1))
1.1813+ (string-to-number (match-string 2)))))))
1.1814+ (when (file-exists-p file)
1.1815+ (delete-file file)))))
1.1816+ (or text-size
1.1817+ ;; This "default" text size is kinda bogus.
1.1818+ (cons (* (length text) font-size) font-size)))))
1.1819+
1.1820+(defun eplot--draw-legend (svg chart)
1.1821+ (with-slots ( legend plots
1.1822+ margin-left margin-top
1.1823+ font font-size font-weight
1.1824+ background-color axes-color
1.1825+ legend-color legend-background-color legend-border-color)
1.1826+ chart
1.1827+ (when (eq legend 'true)
1.1828+ (when-let ((names
1.1829+ (cl-loop for plot in plots
1.1830+ for name = (slot-value plot 'name)
1.1831+ when name
1.1832+ collect
1.1833+ (cons name (slot-value plot 'color)))))
1.1834+ (svg-rectangle svg (+ margin-left 20) (+ margin-top 20)
1.1835+ (format "%dex"
1.1836+ (+ 2
1.1837+ (seq-max (mapcar (lambda (name)
1.1838+ (length (car name)))
1.1839+ names))))
1.1840+ (* font-size (+ (length names) 2))
1.1841+ :font-size font-size
1.1842+ :fill-color legend-background-color
1.1843+ :stroke-color legend-border-color)
1.1844+ (cl-loop for name in names
1.1845+ for i from 0
1.1846+ do (svg-text svg (car name)
1.1847+ :font-family font
1.1848+ :text-anchor "front"
1.1849+ :font-size font-size
1.1850+ :font-weight font-weight
1.1851+ :fill (or (cdr name) legend-color)
1.1852+ :x (+ margin-left 25)
1.1853+ :y (+ margin-top 40 (* i font-size))))))))
1.1854+
1.1855+(defun eplot--format-y (y spacing whole format-string)
1.1856+ (format (or format-string "%s")
1.1857+ (cond
1.1858+ ((or (= (round (* spacing 100)) 10) (= (round (* spacing 100)) 20))
1.1859+ (format "%.1f" y))
1.1860+ ((< spacing 0.01)
1.1861+ (format "%.3f" y))
1.1862+ ((< spacing 1)
1.1863+ (format "%.2f" y))
1.1864+ ((and (< spacing 1) (not (zerop (mod (* spacing 10) 1))))
1.1865+ (format "%.1f" y))
1.1866+ ((zerop (% spacing 1000000000))
1.1867+ (format "%dG" (/ y 1000000000)))
1.1868+ ((zerop (% spacing 1000000))
1.1869+ (format "%dM" (/ y 1000000)))
1.1870+ ((zerop (% spacing 1000))
1.1871+ (format "%dk" (/ y 1000)))
1.1872+ ((>= spacing 1)
1.1873+ (format "%s" y))
1.1874+ ((not whole)
1.1875+ (format "%.1f" y))
1.1876+ (t
1.1877+ (format "%s" y)))))
1.1878+
1.1879+(defun eplot--format-value (value print-format label-format)
1.1880+ (replace-regexp-in-string
1.1881+ ;; Texts in SVG collapse multiple spaces into one. So do it here,
1.1882+ ;; too, so that width calculations are correct.
1.1883+ " +" " "
1.1884+ (cond
1.1885+ ((eq print-format 'date)
1.1886+ (format-time-string
1.1887+ (or label-format "%Y-%m-%d") (eplot--days-to-time value)))
1.1888+ ((eq print-format 'year)
1.1889+ (format-time-string (or label-format "%Y") (eplot--days-to-time value)))
1.1890+ ((eq print-format 'time)
1.1891+ (format-time-string (or label-format "%H:%M:%S") value))
1.1892+ ((eq print-format 'minute)
1.1893+ (format-time-string (or label-format "%H:%M") value))
1.1894+ ((eq print-format 'hour)
1.1895+ (format-time-string (or label-format "%H") value))
1.1896+ (t
1.1897+ (format (or label-format "%s") value)))))
1.1898+
1.1899+(defun eplot--compute-x-ticks (xs x-values print-format x-label-format
1.1900+ label-font label-font-size)
1.1901+ (let* ((min (seq-min x-values))
1.1902+ (max (seq-max x-values))
1.1903+ (count (length x-values))
1.1904+ (max-print (eplot--format-value max print-format x-label-format))
1.1905+ ;; We want each label to be spaced at least as long apart as
1.1906+ ;; the length of the longest label, with room for two blanks
1.1907+ ;; in between.
1.1908+ (min-spacing (* 1.2 (eplot--text-width max-print label-font
1.1909+ label-font-size)))
1.1910+ (digits (eplot--decimal-digits (- (cadr x-values) (car x-values))))
1.1911+ (every (e/ 1 (expt 10 digits))))
1.1912+ (cond
1.1913+ ;; We have room for every X value.
1.1914+ ((< (* count min-spacing) xs)
1.1915+ (list every every))
1.1916+ ;; We have to prune X labels, but not grid lines. (We shouldn't
1.1917+ ;; have a grid line more than every 10 pixels.)
1.1918+ ((< (* count 10) xs)
1.1919+ (list every
1.1920+ (let ((label-step every))
1.1921+ (while (> (/ (- max min) label-step) (/ xs min-spacing))
1.1922+ (setq label-step (eplot--next-weed label-step)))
1.1923+ label-step)))
1.1924+ ;; We have to reduce both grid lines and labels.
1.1925+ (t
1.1926+ (let ((tick-step every))
1.1927+ (while (> (/ (- max min) tick-step) (/ xs 10))
1.1928+ (setq tick-step (eplot--next-weed tick-step)))
1.1929+ (list tick-step
1.1930+ (let ((label-step tick-step))
1.1931+ (while (> (/ (- max min) label-step) (/ xs min-spacing))
1.1932+ (setq label-step (eplot--next-weed label-step))
1.1933+ (while (not (zerop (% label-step tick-step)))
1.1934+ (setq label-step (eplot--next-weed label-step))))
1.1935+ label-step)))))))
1.1936+
1.1937+(defun eplot--compute-y-ticks (ys y-values text-height)
1.1938+ (let* ((min (car y-values))
1.1939+ (max (car (last y-values)))
1.1940+ (count (length y-values))
1.1941+ ;; We want each label to be spaced at least as long apart as
1.1942+ ;; the height of the label.
1.1943+ (min-spacing (+ text-height 10))
1.1944+ (digits (eplot--decimal-digits (- (cadr y-values) (car y-values))))
1.1945+ (every (e/ 1 (expt 10 digits))))
1.1946+ (cond
1.1947+ ;; We have room for every X value.
1.1948+ ((< (* count min-spacing) ys)
1.1949+ (list every every))
1.1950+ ;; We have to prune Y labels, but not grid lines. (We shouldn't
1.1951+ ;; have a grid line more than every 10 pixels.)
1.1952+ ((< (* count 10) ys)
1.1953+ (list every
1.1954+ (let ((label-step every))
1.1955+ (while (> (/ (- max min) label-step) (/ ys min-spacing))
1.1956+ (setq label-step (eplot--next-weed label-step)))
1.1957+ label-step)))
1.1958+ ;; We have to reduce both grid lines and labels.
1.1959+ (t
1.1960+ (let ((tick-step 1))
1.1961+ (while (> (/ count tick-step) (/ ys 10))
1.1962+ (setq tick-step (eplot--next-weed tick-step)))
1.1963+ (list tick-step
1.1964+ (let ((label-step tick-step))
1.1965+ (while (> (/ count label-step) (/ ys min-spacing))
1.1966+ (setq label-step (eplot--next-weed label-step))
1.1967+ (while (not (zerop (% label-step tick-step)))
1.1968+ (setq label-step (eplot--next-weed label-step))))
1.1969+ label-step)))))))
1.1970+
1.1971+(defvar eplot--pleasing-numbers '(1 2 5 10))
1.1972+
1.1973+(defun eplot--next-weed (weed)
1.1974+ (let (digits series)
1.1975+ (if (>= weed 1)
1.1976+ (setq digits (truncate (log weed 10))
1.1977+ series (/ weed (expt 10 digits)))
1.1978+ (setq digits (eplot--decimal-digits weed)
1.1979+ series (truncate (* weed (expt 10 digits)))))
1.1980+ (let ((next (cadr (memq series eplot--pleasing-numbers))))
1.1981+ (unless next
1.1982+ (error "Invalid weed: %s" weed))
1.1983+ (if (>= weed 1)
1.1984+ (* next (expt 10 digits))
1.1985+ (e/ next (expt 10 digits))))))
1.1986+
1.1987+(defun eplot--parse-gradient (string)
1.1988+ (when string
1.1989+ (let ((bits (split-string string)))
1.1990+ (list
1.1991+ (cons 'from (nth 0 bits))
1.1992+ (cons 'to (nth 1 bits))
1.1993+ (cons 'direction (intern (or (nth 2 bits) "top-down")))
1.1994+ (cons 'position (intern (or (nth 3 bits) "below")))))))
1.1995+
1.1996+(defun eplot--smooth (values algo xs)
1.1997+ (if (not algo)
1.1998+ values
1.1999+ (let* ((vals (cl-coerce values 'vector))
1.2000+ (max (1- (length vals)))
1.2001+ (period (* 4 (ceiling (/ max xs)))))
1.2002+ (cl-case algo
1.2003+ (moving-average
1.2004+ (cl-loop for i from 0 upto max
1.2005+ collect (e/ (cl-loop for ii from 0 upto (1- period)
1.2006+ sum (elt vals (min (+ i ii) max)))
1.2007+ period)))))))
1.2008+
1.2009+(defun eplot--vary-color (color n)
1.2010+ (let ((colors ["#e6194b" "#3cb44b" "#ffe119" "#4363d8" "#f58231" "#911eb4"
1.2011+ "#46f0f0" "#f032e6" "#bcf60c" "#fabebe" "#008080" "#e6beff"
1.2012+ "#9a6324" "#fffac8" "#800000" "#aaffc3" "#808000" "#ffd8b1"
1.2013+ "#000075" "#808080" "#ffffff" "#000000"]))
1.2014+ (unless (equal color "vary")
1.2015+ (setq colors
1.2016+ (if (string-search " " color)
1.2017+ (split-string color)
1.2018+ (list color))))
1.2019+ (elt colors (mod n (length colors)))))
1.2020+
1.2021+(defun eplot--pv (plot slot &optional default)
1.2022+ (let ((user (cdr (assq slot eplot--user-defaults))))
1.2023+ (when (and (stringp user) (zerop (length user)))
1.2024+ (setq user nil))
1.2025+ (or user (slot-value plot slot) default)))
1.2026+
1.2027+(defun eplot--draw-plots (svg chart)
1.2028+ (if (eq (slot-value chart 'format) 'horizontal-bar-chart)
1.2029+ (eplot--draw-horizontal-bar-chart svg chart)
1.2030+ (eplot--draw-normal-plots svg chart)))
1.2031+
1.2032+(defun eplot--draw-normal-plots (svg chart)
1.2033+ (with-slots ( plots chart-color height format
1.2034+ margin-bottom margin-left
1.2035+ min max xs ys
1.2036+ margin-top
1.2037+ x-values x-min x-max
1.2038+ label-font label-font-size)
1.2039+ chart
1.2040+ ;; Draw all the plots.
1.2041+ (cl-loop for plot in (reverse plots)
1.2042+ for plot-number from 0
1.2043+ for values = (slot-value plot 'values)
1.2044+ for stride = (eplot--stride chart values)
1.2045+ for vals = (eplot--smooth
1.2046+ (seq-map (lambda (v) (plist-get v :value)) values)
1.2047+ (slot-value plot 'smoothing)
1.2048+ xs)
1.2049+ for polygon = nil
1.2050+ for gradient = (eplot--parse-gradient (eplot--pv plot 'gradient))
1.2051+ for lpy = nil
1.2052+ for lpx = nil
1.2053+ for style = (if (eq format 'bar-chart)
1.2054+ 'bar
1.2055+ (slot-value plot 'style))
1.2056+ for bar-gap = (* stride 0.1)
1.2057+ for clip-id = (format "url(#clip-%d)" plot-number)
1.2058+ do
1.2059+ (svg--append
1.2060+ svg
1.2061+ (dom-node 'clipPath
1.2062+ `((id . ,(format "clip-%d" plot-number)))
1.2063+ (dom-node 'rect
1.2064+ `((x . ,margin-left)
1.2065+ (y . , margin-top)
1.2066+ (width . ,xs)
1.2067+ (height . ,ys)))))
1.2068+ (unless gradient
1.2069+ (when-let ((fill (slot-value plot 'fill-color)))
1.2070+ (setq gradient `((from . ,fill) (to . ,fill)
1.2071+ (direction . top-down) (position . below)))))
1.2072+ (when gradient
1.2073+ (if (eq (eplot--vs 'position gradient) 'above)
1.2074+ (push (cons margin-left margin-top) polygon)
1.2075+ (push (cons margin-left (- height margin-bottom)) polygon)))
1.2076+ (cl-loop
1.2077+ for val in vals
1.2078+ for value in values
1.2079+ for x in x-values
1.2080+ for i from 0
1.2081+ for settings = (plist-get value :settings)
1.2082+ for color = (eplot--vary-color
1.2083+ (eplot--vs 'color settings (slot-value plot 'color))
1.2084+ i)
1.2085+ for py = (- (- height margin-bottom)
1.2086+ (* (/ (- (* 1.0 val) min) (- max min))
1.2087+ ys))
1.2088+ for px = (if (eq style 'bar)
1.2089+ (+ margin-left
1.2090+ (* (e/ (- x x-min) (- x-max x-min -1))
1.2091+ xs))
1.2092+ (+ margin-left
1.2093+ (* (e/ (- x x-min) (- x-max x-min))
1.2094+ xs)))
1.2095+ do
1.2096+ ;; Some data points may have texts.
1.2097+ (when-let ((text (eplot--vs 'text settings)))
1.2098+ (svg-text svg text
1.2099+ :font-family label-font
1.2100+ :text-anchor "middle"
1.2101+ :font-size label-font-size
1.2102+ :font-weight 'normal
1.2103+ :fill color
1.2104+ :x px
1.2105+ :y (- py (eplot--text-height
1.2106+ text label-font label-font-size)
1.2107+ -5)))
1.2108+ ;; You may mark certain points.
1.2109+ (when-let ((mark (eplot--vy 'mark settings)))
1.2110+ (cl-case mark
1.2111+ (cross
1.2112+ (let ((s (eplot--element-size val plot settings 3)))
1.2113+ (svg-line svg (- px s) (- py s)
1.2114+ (+ px s) (+ py s)
1.2115+ :clip-path clip-id
1.2116+ :stroke color)
1.2117+ (svg-line svg (+ px s) (- py s)
1.2118+ (- px s) (+ py s)
1.2119+ :clip-path clip-id
1.2120+ :stroke color)))
1.2121+ (otherwise
1.2122+ (svg-circle svg px py 3
1.2123+ :fill color))))
1.2124+ (cl-case style
1.2125+ (bar
1.2126+ (if (not gradient)
1.2127+ (svg-rectangle
1.2128+ svg (+ px bar-gap) py
1.2129+ (- stride bar-gap) (- height margin-bottom py)
1.2130+ :clip-path clip-id
1.2131+ :fill color)
1.2132+ (let ((id (format "gradient-%s" (make-temp-name "grad"))))
1.2133+ (eplot--gradient svg id 'linear
1.2134+ (eplot--stops (eplot--vs 'from gradient)
1.2135+ (eplot--vs 'to gradient))
1.2136+ (eplot--vs 'direction gradient))
1.2137+ (svg-rectangle
1.2138+ svg (+ px bar-gap) py
1.2139+ (- stride bar-gap) (- height margin-bottom py)
1.2140+ :clip-path clip-id
1.2141+ :gradient id))))
1.2142+ (impulse
1.2143+ (let ((width (eplot--element-size val plot settings 1)))
1.2144+ (if (= width 1)
1.2145+ (svg-line svg
1.2146+ px py
1.2147+ px (- height margin-bottom)
1.2148+ :clip-path clip-id
1.2149+ :stroke color)
1.2150+ (svg-rectangle svg
1.2151+ (- px (e/ width 2)) py
1.2152+ width (- height py margin-bottom)
1.2153+ :clip-path clip-id
1.2154+ :fill color))))
1.2155+ (point
1.2156+ (svg-line svg px py (1+ px) (1+ py)
1.2157+ :clip-path clip-id
1.2158+ :stroke color))
1.2159+ (line
1.2160+ ;; If we're doing a gradient, we're just collecting
1.2161+ ;; points and will draw the polygon later.
1.2162+ (if gradient
1.2163+ (push (cons px py) polygon)
1.2164+ (when lpx
1.2165+ (svg-line svg lpx lpy px py
1.2166+ :stroke-width (eplot--pv plot 'size 1)
1.2167+ :clip-path clip-id
1.2168+ :stroke color))))
1.2169+ (curve
1.2170+ (push (cons px py) polygon))
1.2171+ (square
1.2172+ (if gradient
1.2173+ (progn
1.2174+ (when lpx
1.2175+ (push (cons lpx py) polygon))
1.2176+ (push (cons px py) polygon))
1.2177+ (when lpx
1.2178+ (svg-line svg lpx lpy px lpy
1.2179+ :clip-path clip-id
1.2180+ :stroke color)
1.2181+ (svg-line svg px lpy px py
1.2182+ :clip-path clip-id
1.2183+ :stroke color))))
1.2184+ (circle
1.2185+ (svg-circle svg px py
1.2186+ (eplot--element-size val plot settings 3)
1.2187+ :clip-path clip-id
1.2188+ :stroke color
1.2189+ :fill (eplot--vary-color
1.2190+ (eplot--vs
1.2191+ 'fill-color settings
1.2192+ (or (slot-value plot 'fill-color) "none"))
1.2193+ i)))
1.2194+ (cross
1.2195+ (let ((s (eplot--element-size val plot settings 3)))
1.2196+ (svg-line svg (- px s) (- py s)
1.2197+ (+ px s) (+ py s)
1.2198+ :clip-path clip-id
1.2199+ :stroke color)
1.2200+ (svg-line svg (+ px s) (- py s)
1.2201+ (- px s) (+ py s)
1.2202+ :clip-path clip-id
1.2203+ :stroke color)))
1.2204+ (triangle
1.2205+ (let ((s (eplot--element-size val plot settings 5)))
1.2206+ (svg-polygon svg
1.2207+ (list
1.2208+ (cons (- px (e/ s 2)) (+ py (e/ s 2)))
1.2209+ (cons px (- py (e/ s 2)))
1.2210+ (cons (+ px (e/ s 2)) (+ py (e/ s 2))))
1.2211+ :clip-path clip-id
1.2212+ :stroke color
1.2213+ :fill-color
1.2214+ (or (slot-value plot 'fill-color) "none"))))
1.2215+ (rectangle
1.2216+ (let ((s (eplot--element-size val plot settings 3)))
1.2217+ (svg-rectangle svg (- px (e/ s 2)) (- py (e/ s 2))
1.2218+ s s
1.2219+ :clip-path clip-id
1.2220+ :stroke color
1.2221+ :fill-color
1.2222+ (or (slot-value plot 'fill-color) "none")))))
1.2223+ (setq lpy py
1.2224+ lpx px))
1.2225+
1.2226+ ;; We're doing a gradient of some kind (or a curve), so
1.2227+ ;; draw it now when we've collected the polygon.
1.2228+ (when polygon
1.2229+ ;; We have a "between" chart, so collect the data points
1.2230+ ;; from the "extra" values, too.
1.2231+ (when (memq 'two-values (slot-value plot 'data-format))
1.2232+ (cl-loop
1.2233+ for val in (nreverse
1.2234+ (seq-map (lambda (v) (plist-get v :extra-value))
1.2235+ values))
1.2236+ for x from (1- (length vals)) downto 0
1.2237+ for py = (- (- height margin-bottom)
1.2238+ (* (/ (- (* 1.0 val) min) (- max min))
1.2239+ ys))
1.2240+ for px = (+ margin-left
1.2241+ (* (e/ (- x x-min) (- x-max x-min))
1.2242+ xs))
1.2243+ do
1.2244+ (cl-case style
1.2245+ (line
1.2246+ (push (cons px py) polygon))
1.2247+ (square
1.2248+ (when lpx
1.2249+ (push (cons lpx py) polygon))
1.2250+ (push (cons px py) polygon)))
1.2251+ (setq lpx px lpy py)))
1.2252+ (when gradient
1.2253+ (if (eq (eplot--vs 'position gradient) 'above)
1.2254+ (push (cons lpx margin-top) polygon)
1.2255+ (push (cons lpx (- height margin-bottom)) polygon)))
1.2256+ (let ((id (format "gradient-%d" plot-number)))
1.2257+ (when gradient
1.2258+ (eplot--gradient svg id 'linear
1.2259+ (eplot--stops (eplot--vs 'from gradient)
1.2260+ (eplot--vs 'to gradient))
1.2261+ (eplot--vs 'direction gradient)))
1.2262+ (if (eq style 'curve)
1.2263+ (apply #'svg-path svg
1.2264+ (nconc
1.2265+ (cl-loop
1.2266+ with points = (cl-coerce
1.2267+ (nreverse polygon) 'vector)
1.2268+ for i from 0 upto (1- (length points))
1.2269+ collect
1.2270+ (cond
1.2271+ ((zerop i)
1.2272+ `(moveto ((,(car (elt points 0)) .
1.2273+ ,(cdr (elt points 0))))))
1.2274+ (t
1.2275+ `(curveto
1.2276+ (,(eplot--bezier
1.2277+ (eplot--pv plot 'bezier-factor)
1.2278+ i points))))))
1.2279+ (and gradient '((closepath))))
1.2280+ `( :clip-path ,clip-id
1.2281+ :stroke-width ,(eplot--pv plot 'size 1)
1.2282+ :stroke ,(slot-value plot 'color)
1.2283+ ,@(if gradient
1.2284+ `(:gradient ,id)
1.2285+ `(:fill "none"))))
1.2286+ (svg-polygon
1.2287+ svg (nreverse polygon)
1.2288+ :clip-path clip-id
1.2289+ :gradient id
1.2290+ :stroke (slot-value plot 'fill-border-color))))))))
1.2291+
1.2292+(defun eplot--element-size (value plot settings default)
1.2293+ (eplot--vn 'size settings
1.2294+ (if (slot-value plot 'size-factor)
1.2295+ (* value (slot-value plot 'size-factor))
1.2296+ (or (slot-value plot 'size) default))))
1.2297+
1.2298+(defun eplot--draw-horizontal-bar-chart (svg chart)
1.2299+ (with-slots ( plots chart-color height format
1.2300+ margin-bottom margin-left
1.2301+ min max xs ys
1.2302+ margin-top
1.2303+ x-values x-min x-max
1.2304+ label-font label-font-size label-color)
1.2305+ chart
1.2306+ (cl-loop with plot = (car plots)
1.2307+ with values = (slot-value plot 'values)
1.2308+ with stride = (e/ ys (length values))
1.2309+ with label-height = (eplot--text-height "xx" label-font
1.2310+ label-font-size)
1.2311+ with bar-gap = (* stride 0.1)
1.2312+ for i from 0
1.2313+ for value in values
1.2314+ for settings = (plist-get value :settings)
1.2315+ for py = (+ margin-top (* i stride))
1.2316+ for px = (* (e/ (plist-get value :x) x-max) xs)
1.2317+ for color = (eplot--vary-color
1.2318+ (eplot--vs 'color settings (slot-value plot 'color))
1.2319+ i)
1.2320+ do
1.2321+ (svg-text svg (eplot--vs 'label settings)
1.2322+ :font-family label-font
1.2323+ :text-anchor "left"
1.2324+ :font-size label-font-size
1.2325+ :font-weight 'normal
1.2326+ :fill label-color
1.2327+ :x 5
1.2328+ :y (+ py label-height (/ (- stride label-height) 2)))
1.2329+ (svg-rectangle svg
1.2330+ margin-left (+ py (e/ bar-gap 2))
1.2331+ px (- stride bar-gap)
1.2332+ :fill color))))
1.2333+
1.2334+(defun eplot--stops (from to)
1.2335+ (append `((0 . ,from))
1.2336+ (cl-loop for (pct col) on (split-string to "-") by #'cddr
1.2337+ collect (if col
1.2338+ (cons (string-to-number pct) col)
1.2339+ (cons 100 pct)))))
1.2340+
1.2341+(defun eplot--gradient (svg id type stops &optional direction)
1.2342+ "Add a gradient with ID to SVG.
1.2343+TYPE is `linear' or `radial'.
1.2344+
1.2345+STOPS is a list of percentage/color pairs.
1.2346+
1.2347+DIRECTION is one of `top-down', `bottom-up', `left-right' or `right-left'.
1.2348+nil means `top-down'."
1.2349+ (svg--def
1.2350+ svg
1.2351+ (apply
1.2352+ #'dom-node
1.2353+ (if (eq type 'linear)
1.2354+ 'linearGradient
1.2355+ 'radialGradient)
1.2356+ `((id . ,id)
1.2357+ (x1 . ,(if (eq direction 'left-right) 1 0))
1.2358+ (x2 . ,(if (eq direction 'right-left) 1 0))
1.2359+ (y1 . ,(if (eq direction 'bottom-up) 1 0))
1.2360+ (y2 . ,(if (eq direction 'top-down) 1 0)))
1.2361+ (mapcar
1.2362+ (lambda (stop)
1.2363+ (dom-node 'stop `((offset . ,(format "%s%%" (car stop)))
1.2364+ (stop-color . ,(cdr stop)))))
1.2365+ stops))))
1.2366+
1.2367+(defun e% (num1 num2)
1.2368+ (let ((factor (max (expt 10 (eplot--decimal-digits num1))
1.2369+ (expt 10 (eplot--decimal-digits num2)))))
1.2370+ (% (truncate (* num1 factor)) (truncate (* num2 factor)))))
1.2371+
1.2372+(defun eplot--decimal-digits (number)
1.2373+ (- (length (replace-regexp-in-string
1.2374+ "0+\\'" ""
1.2375+ (format "%.10f" (- number (truncate number)))))
1.2376+ 2))
1.2377+
1.2378+(defun e/ (&rest numbers)
1.2379+ (if (cl-every #'integerp numbers)
1.2380+ (let ((int (apply #'/ numbers))
1.2381+ (float (apply #'/ (* 1.0 (car numbers)) (cdr numbers))))
1.2382+ (if (= int float)
1.2383+ int
1.2384+ float))
1.2385+ (apply #'/ numbers)))
1.2386+
1.2387+(defun eplot--get-ticks (min max height &optional whole)
1.2388+ (let* ((diff (abs (- min max)))
1.2389+ (even (eplot--pleasing-numbers (* (e/ diff height) 10)))
1.2390+ (factor (max (expt 10 (eplot--decimal-digits even))
1.2391+ (expt 10 (eplot--decimal-digits diff))))
1.2392+ (fmin (truncate (* min factor)))
1.2393+ (feven (truncate (* factor even)))
1.2394+ start)
1.2395+ (when whole
1.2396+ (setq even 1
1.2397+ feven factor))
1.2398+
1.2399+ (setq start
1.2400+ (cond
1.2401+ ((< min 0)
1.2402+ (+ (floor fmin)
1.2403+ feven
1.2404+ (- (% (floor fmin) feven))
1.2405+ (- feven)))
1.2406+ (t
1.2407+ (- fmin (% fmin feven)))))
1.2408+ (cl-loop for x from start upto (* max factor) by feven
1.2409+ collect (e/ x factor))))
1.2410+
1.2411+(defun eplot--days-to-time (days)
1.2412+ (days-to-time (- days (time-to-days 0))))
1.2413+
1.2414+(defun eplot--get-date-ticks (start end xs label-font label-font-size
1.2415+ x-label-format &optional skip-until)
1.2416+ (let* ((duration (- end start))
1.2417+ (limits
1.2418+ (list
1.2419+ (list (/ 368 16) 'date
1.2420+ (lambda (_d) t))
1.2421+ (list (/ 368 4) 'date
1.2422+ ;; Collect Mondays.
1.2423+ (lambda (decoded)
1.2424+ (= (decoded-time-weekday decoded) 1)))
1.2425+ (list (/ 368 2) 'date
1.2426+ ;; Collect 1st and 15th.
1.2427+ (lambda (decoded)
1.2428+ (or (= (decoded-time-day decoded) 1)
1.2429+ (= (decoded-time-day decoded) 15))))
1.2430+ (list (* 368 2) 'date
1.2431+ ;; Collect 1st of every month.
1.2432+ (lambda (decoded)
1.2433+ (= (decoded-time-day decoded) 1)))
1.2434+ (list (* 368 4) 'date
1.2435+ ;; Collect every quarter.
1.2436+ (lambda (decoded)
1.2437+ (and (= (decoded-time-day decoded) 1)
1.2438+ (memq (decoded-time-month decoded) '(1 4 7 10)))))
1.2439+ (list (* 368 8) 'date
1.2440+ ;; Collect every half year.
1.2441+ (lambda (decoded)
1.2442+ (and (= (decoded-time-day decoded) 1)
1.2443+ (memq (decoded-time-month decoded) '(1 7)))))
1.2444+ (list 1.0e+INF 'year
1.2445+ ;; Collect every Jan 1st.
1.2446+ (lambda (decoded)
1.2447+ (and (= (decoded-time-day decoded) 1)
1.2448+ (= (decoded-time-month decoded) 1)))))))
1.2449+ ;; First we collect the potential ticks.
1.2450+ (while (or (>= duration (caar limits))
1.2451+ (and skip-until (>= skip-until (caar limits))))
1.2452+ (pop limits))
1.2453+ (let* ((x-ticks (cl-loop for day from start upto end
1.2454+ for time = (eplot--days-to-time day)
1.2455+ for decoded = (decode-time time)
1.2456+ when (funcall (nth 2 (car limits)) decoded)
1.2457+ collect day))
1.2458+ (count (length x-ticks))
1.2459+ (print-format (nth 1 (car limits)))
1.2460+ (max-print (eplot--format-value (car x-ticks) print-format
1.2461+ x-label-format))
1.2462+ (min-spacing (* 1.2 (eplot--text-width max-print label-font
1.2463+ label-font-size))))
1.2464+ (cond
1.2465+ ;; We have room for every X value.
1.2466+ ((< (* count min-spacing) xs)
1.2467+ (list x-ticks print-format))
1.2468+ ;; We have to prune X labels, but not grid lines. (We shouldn't
1.2469+ ;; have a grid line more than every 10 pixels.)
1.2470+ ((< (* count 10) xs)
1.2471+ (cond
1.2472+ ((not (cdr limits))
1.2473+ (eplot--year-ticks
1.2474+ x-ticks xs label-font label-font-size x-label-format))
1.2475+ ;; The Mondays grid is special, because it doesn't resolve
1.2476+ ;; into any of the bigger limits evenly.
1.2477+ ((= (caar limits) (/ 368 4))
1.2478+ (let* ((max-print (eplot--format-value
1.2479+ (car x-ticks) print-format x-label-format))
1.2480+ (min-spacing (* 1.2 (eplot--text-width
1.2481+ max-print label-font label-font-size)))
1.2482+ (weed-factor 2))
1.2483+ (while (> (* (/ (length x-ticks) weed-factor) min-spacing) xs)
1.2484+ (setq weed-factor (* weed-factor 2)))
1.2485+ (list x-ticks 'date
1.2486+ (cl-loop for val in x-ticks
1.2487+ for i from 0
1.2488+ collect (list val t (zerop (% i weed-factor)))))))
1.2489+ (t
1.2490+ (pop limits)
1.2491+ (catch 'found
1.2492+ (while limits
1.2493+ (let ((candidate
1.2494+ (cl-loop for day in x-ticks
1.2495+ for time = (eplot--days-to-time day)
1.2496+ for decoded = (decode-time time)
1.2497+ collect (list day t
1.2498+ (not (not
1.2499+ (funcall (nth 2 (car limits))
1.2500+ decoded)))))))
1.2501+ (setq print-format (nth 1 (car limits)))
1.2502+ (let* ((max-print (eplot--format-value
1.2503+ (car x-ticks) print-format x-label-format))
1.2504+ (min-spacing (* 1.2 (eplot--text-width
1.2505+ max-print label-font
1.2506+ label-font-size)))
1.2507+ (num-labels (seq-count (lambda (v) (nth 2 v))
1.2508+ candidate)))
1.2509+ (when (and (not (zerop num-labels))
1.2510+ (< (* num-labels min-spacing) xs))
1.2511+ (throw 'found (list x-ticks print-format candidate)))))
1.2512+ (pop limits))
1.2513+ (eplot--year-ticks
1.2514+ x-ticks xs label-font label-font-size x-label-format)))))
1.2515+ ;; We have to reduce both grid lines and labels.
1.2516+ (t
1.2517+ (eplot--get-date-ticks start end xs label-font label-font-size
1.2518+ x-label-format (caar limits)))))))
1.2519+
1.2520+(defun eplot--year-ticks (x-ticks xs label-font label-font-size x-label-format)
1.2521+ (let* ((year-ticks (mapcar (lambda (day)
1.2522+ (decoded-time-year
1.2523+ (decode-time (eplot--days-to-time day))))
1.2524+ x-ticks))
1.2525+ (xv (eplot--compute-x-ticks
1.2526+ xs year-ticks 'year x-label-format label-font label-font-size)))
1.2527+ (let ((tick-step (car xv))
1.2528+ (label-step (cadr xv)))
1.2529+ (list x-ticks 'year
1.2530+ (cl-loop for year in year-ticks
1.2531+ for val in x-ticks
1.2532+ collect (list val
1.2533+ (zerop (% year tick-step))
1.2534+ (zerop (% year label-step))))))))
1.2535+
1.2536+(defun eplot--get-time-ticks (start end xs label-font label-font-size
1.2537+ x-label-format
1.2538+ &optional skip-until)
1.2539+ (let* ((duration (- end start))
1.2540+ (limits
1.2541+ (list
1.2542+ (list (* 2 60) 'time
1.2543+ (lambda (_d) t))
1.2544+ (list (* 2 60 60) 'time
1.2545+ ;; Collect whole minutes.
1.2546+ (lambda (decoded)
1.2547+ (zerop (decoded-time-second decoded))))
1.2548+ (list (* 3 60 60) 'minute
1.2549+ ;; Collect five minutes.
1.2550+ (lambda (decoded)
1.2551+ (zerop (% (decoded-time-minute decoded) 5))))
1.2552+ (list (* 4 60 60) 'minute
1.2553+ ;; Collect fifteen minutes.
1.2554+ (lambda (decoded)
1.2555+ (and (zerop (decoded-time-second decoded))
1.2556+ (memq (decoded-time-minute decoded) '(0 15 30 45)))))
1.2557+ (list (* 8 60 60) 'minute
1.2558+ ;; Collect half hours.
1.2559+ (lambda (decoded)
1.2560+ (and (zerop (decoded-time-second decoded))
1.2561+ (memq (decoded-time-minute decoded) '(0 30)))))
1.2562+ (list 1.0e+INF 'hour
1.2563+ ;; Collect whole hours.
1.2564+ (lambda (decoded)
1.2565+ (and (zerop (decoded-time-second decoded))
1.2566+ (zerop (decoded-time-minute decoded))))))))
1.2567+ ;; First we collect the potential ticks.
1.2568+ (while (or (>= duration (caar limits))
1.2569+ (and skip-until (>= skip-until (caar limits))))
1.2570+ (pop limits))
1.2571+ (let* ((x-ticks (cl-loop for time from start upto end
1.2572+ for decoded = (decode-time time)
1.2573+ when (funcall (nth 2 (car limits)) decoded)
1.2574+ collect time))
1.2575+ (count (length x-ticks))
1.2576+ (print-format (nth 1 (car limits)))
1.2577+ (max-print (eplot--format-value (car x-ticks) print-format
1.2578+ x-label-format))
1.2579+ (min-spacing (* (+ (length max-print) 2) (e/ label-font-size 2))))
1.2580+ (cond
1.2581+ ;; We have room for every X value.
1.2582+ ((< (* count min-spacing) xs)
1.2583+ (list x-ticks print-format))
1.2584+ ;; We have to prune X labels, but not grid lines. (We shouldn't
1.2585+ ;; have a grid line more than every 10 pixels.)
1.2586+ ;; If we're plotting just seconds, then just weed out some seconds.
1.2587+ ((and (< (* count 10) xs)
1.2588+ (= (caar limits) (* 2 60)))
1.2589+ (let ((xv (eplot--compute-x-ticks
1.2590+ xs x-ticks 'time x-label-format label-font label-font-size)))
1.2591+ (let ((tick-step (car xv))
1.2592+ (label-step (cadr xv)))
1.2593+ (list x-ticks 'time
1.2594+ (cl-loop for val in x-ticks
1.2595+ collect (list val
1.2596+ (zerop (% val tick-step))
1.2597+ (zerop (% val label-step))))))))
1.2598+ ;; Normal case for pruning labels, but not grid lines.
1.2599+ ((< (* count 10) xs)
1.2600+ (if (not (cdr limits))
1.2601+ (eplot--hour-ticks x-ticks xs label-font label-font-size
1.2602+ x-label-format)
1.2603+ (pop limits)
1.2604+ (catch 'found
1.2605+ (while limits
1.2606+ (let ((candidate
1.2607+ (cl-loop for val in x-ticks
1.2608+ for decoded = (decode-time val)
1.2609+ collect (list val t
1.2610+ (not (not
1.2611+ (funcall (nth 2 (car limits))
1.2612+ decoded)))))))
1.2613+ (setq print-format (nth 1 (car limits)))
1.2614+ (let ((min-spacing (* (+ (length max-print) 2)
1.2615+ (e/ label-font-size 2))))
1.2616+ (when (< (* (seq-count (lambda (v) (nth 2 v)) candidate)
1.2617+ min-spacing)
1.2618+ xs)
1.2619+ (throw 'found (list x-ticks print-format candidate)))))
1.2620+ (pop limits))
1.2621+ (eplot--hour-ticks x-ticks xs label-font label-font-size
1.2622+ x-label-format))))
1.2623+ ;; We have to reduce both grid lines and labels.
1.2624+ (t
1.2625+ (eplot--get-time-ticks start end xs label-font label-font-size
1.2626+ x-label-format (caar limits)))))))
1.2627+
1.2628+(defun eplot--hour-ticks (x-ticks xs label-font label-font-size
1.2629+ x-label-format)
1.2630+ (let* ((eplot--pleasing-numbers '(1 3 6 12))
1.2631+ (hour-ticks (mapcar (lambda (time)
1.2632+ (decoded-time-hour (decode-time time)))
1.2633+ x-ticks))
1.2634+ (xv (eplot--compute-x-ticks
1.2635+ xs hour-ticks 'year x-label-format label-font label-font-size)))
1.2636+ (let ((tick-step (car xv))
1.2637+ (label-step (cadr xv)))
1.2638+ (list x-ticks 'hour
1.2639+ (cl-loop for hour in hour-ticks
1.2640+ for val in x-ticks
1.2641+ collect (list val
1.2642+ (zerop (% hour tick-step))
1.2643+ (zerop (% hour label-step))))))))
1.2644+
1.2645+(defun eplot--int (number)
1.2646+ (cond
1.2647+ ((integerp number)
1.2648+ number)
1.2649+ ((= number (truncate number))
1.2650+ (truncate number))
1.2651+ (t
1.2652+ number)))
1.2653+
1.2654+(defun eplot--pleasing-numbers (number)
1.2655+ (let* ((digits (eplot--decimal-digits number))
1.2656+ (one (e/ 1 (expt 10 digits)))
1.2657+ (two (e/ 2 (expt 10 digits)))
1.2658+ (five (e/ 5 (expt 10 digits))))
1.2659+ (catch 'found
1.2660+ (while t
1.2661+ (when (< number one)
1.2662+ (throw 'found one))
1.2663+ (setq one (* one 10))
1.2664+ (when (< number two)
1.2665+ (throw 'found two))
1.2666+ (setq two (* two 10))
1.2667+ (when (< number five)
1.2668+ (throw 'found five))
1.2669+ (setq five (* five 10))))))
1.2670+
1.2671+(defun eplot-parse-and-insert (file)
1.2672+ "Parse and insert a file in the current buffer."
1.2673+ (interactive "fEplot file: ")
1.2674+ (let ((default-directory (file-name-directory file)))
1.2675+ (setq-local eplot--current-chart
1.2676+ (eplot--render (with-temp-buffer
1.2677+ (insert-file-contents file)
1.2678+ (eplot--parse-buffer))))))
1.2679+
1.2680+(defun eplot-list-chart-headers ()
1.2681+ "Pop to a buffer showing all chart parameters."
1.2682+ (interactive)
1.2683+ (pop-to-buffer "*eplot help*")
1.2684+ (let ((inhibit-read-only t))
1.2685+ (special-mode)
1.2686+ (erase-buffer)
1.2687+ (insert "The following headers influence the overall\nlook of the chart:\n\n")
1.2688+ (eplot--list-headers eplot--chart-headers)
1.2689+ (ensure-empty-lines 2)
1.2690+ (insert "The following headers are per plot:\n\n")
1.2691+ (eplot--list-headers eplot--plot-headers)
1.2692+ (goto-char (point-min))))
1.2693+
1.2694+(defun eplot--list-headers (headers)
1.2695+ (dolist (header (sort (copy-sequence headers)
1.2696+ (lambda (e1 e2)
1.2697+ (string< (car e1) (car e2)))))
1.2698+ (insert (propertize (capitalize (symbol-name (car header))) 'face 'bold)
1.2699+ "\n")
1.2700+ (let ((start (point)))
1.2701+ (insert (plist-get (cdr header) :doc) "\n")
1.2702+ (when-let ((valid (plist-get (cdr header) :valid)))
1.2703+ (insert "Possible values are: "
1.2704+ (mapconcat (lambda (v) (format "`%s'" v)) valid ", ")
1.2705+ ".\n"))
1.2706+ (indent-rigidly start (point) 2))
1.2707+ (ensure-empty-lines 1)))
1.2708+
1.2709+(defvar eplot--transients
1.2710+ '((("Size"
1.2711+ ("sw" "Width")
1.2712+ ("sh" "Height")
1.2713+ ("sl" "Margin-Left")
1.2714+ ("st" "Margin-Top")
1.2715+ ("sr" "Margin-Right")
1.2716+ ("sb" "Margin-Bottom"))
1.2717+ ("Colors"
1.2718+ ("ca" "Axes-Color")
1.2719+ ("cb" "Border-Color")
1.2720+ ("cc" "Chart-Color")
1.2721+ ("cf" "Frame-Color")
1.2722+ ("cs" "Surround-Color")
1.2723+ ("ct" "Title-Color"))
1.2724+ ("Background"
1.2725+ ("bc" "Background-Color")
1.2726+ ("bg" "Background-Gradient")
1.2727+ ("bi" "Background-Image-File")
1.2728+ ("bv" "Background-Image-Cover")
1.2729+ ("bo" "Background-Image-Opacity")))
1.2730+ (("General"
1.2731+ ("gt" "Title")
1.2732+ ("gf" "Font")
1.2733+ ("gs" "Font-Size")
1.2734+ ("ge" "Font-Weight")
1.2735+ ("go" "Format")
1.2736+ ("gw" "Frame-Width")
1.2737+ ("gh" "Header-File")
1.2738+ ("gi" "Min")
1.2739+ ("ga" "Max")
1.2740+ ("gm" "Mode")
1.2741+ ("gr" "Reset" eplot--reset-transient)
1.2742+ ("gv" "Save" eplot--save-transient))
1.2743+ ("Axes, Grid & Legend"
1.2744+ ("xx" "X-Title")
1.2745+ ("xy" "Y-Title")
1.2746+ ("xf" "Label-Font")
1.2747+ ("xz" "Label-Font-Size")
1.2748+ ("xs" "X-Axis-Title-Space")
1.2749+ ("xl" "X-Label-Format")
1.2750+ ("xa" "Y-Label-Format")
1.2751+ ("il" "Grid-Color")
1.2752+ ("io" "Grid-Opacity")
1.2753+ ("ip" "Grid-Position")
1.2754+ ("ll" "Legend")
1.2755+ ("lb" "Legend-Background-Color")
1.2756+ ("lo" "Legend-Border-Color")
1.2757+ ("lc" "Legend-Color"))
1.2758+ ("Plot"
1.2759+ ("ps" "Style")
1.2760+ ("pc" "Color")
1.2761+ ("po" "Data-Column")
1.2762+ ("pr" "Data-format")
1.2763+ ("pn" "Fill-Border-Color")
1.2764+ ("pi" "Fill-Color")
1.2765+ ("pg" "Gradient")
1.2766+ ("pz" "Size")
1.2767+ ("pm" "Smoothing")
1.2768+ ("pb" "Bezier-Factor")))))
1.2769+
1.2770+(defun eplot--define-transients ()
1.2771+ (cl-loop for row in eplot--transients
1.2772+ collect (cl-coerce
1.2773+ (cl-loop for column in row
1.2774+ collect
1.2775+ (cl-coerce
1.2776+ (cons (pop column)
1.2777+ (mapcar #'eplot--define-transient column))
1.2778+ 'vector))
1.2779+ 'vector)))
1.2780+
1.2781+(defun eplot--define-transient (action)
1.2782+ (list (nth 0 action)
1.2783+ (nth 1 action)
1.2784+ ;; Allow explicit commands.
1.2785+ (or (nth 2 action)
1.2786+ ;; Make a command for altering a setting.
1.2787+ (lambda ()
1.2788+ (interactive)
1.2789+ (eplot--execute-transient (nth 1 action))))))
1.2790+
1.2791+(defun eplot--execute-transient (action)
1.2792+ (with-current-buffer (or eplot--data-buffer (current-buffer))
1.2793+ (unless eplot--transient-settings
1.2794+ (setq-local eplot--transient-settings nil))
1.2795+ (let* ((name (intern (downcase action)))
1.2796+ (spec (assq name (append eplot--chart-headers eplot--plot-headers)))
1.2797+ (type (plist-get (cdr spec) :type)))
1.2798+ ;; Sanity check.
1.2799+ (unless spec
1.2800+ (error "No such header type: %s" name))
1.2801+ (setq eplot--transient-settings
1.2802+ (append
1.2803+ eplot--transient-settings
1.2804+ (list
1.2805+ (cons
1.2806+ name
1.2807+ (cond
1.2808+ ((eq type 'number)
1.2809+ (read-number (format "Value for %s (%s): " action type)))
1.2810+ ((string-match "color" (downcase action))
1.2811+ (eplot--read-color (format "Value for %s (color): " action)))
1.2812+ ((string-match "font" (downcase action))
1.2813+ (eplot--read-font-family
1.2814+ (format "Value for %s (font family): " action)))
1.2815+ ((string-match "gradient" (downcase action))
1.2816+ (eplot--read-gradient action))
1.2817+ ((string-match "file" (downcase action))
1.2818+ (read-file-name (format "File for %s: " action)))
1.2819+ ((eq type 'symbol)
1.2820+ (intern
1.2821+ (completing-read (format "Value for %s: " action)
1.2822+ (plist-get (cdr spec) :valid)
1.2823+ nil t)))
1.2824+ (t
1.2825+ (read-string (format "Value for %s (string): " action))))))))
1.2826+ (eplot-update-view-buffer))))
1.2827+
1.2828+(defun eplot--read-gradient (action)
1.2829+ (format "%s %s %s %s"
1.2830+ (eplot--read-color (format "%s from color: " action))
1.2831+ (eplot--read-color (format "%s to color: " action))
1.2832+ (completing-read (format "%s direction: " action)
1.2833+ '(top-down bottom-up left-right right-left)
1.2834+ nil t)
1.2835+ (completing-read (format "%s position: " action)
1.2836+ '(below above)
1.2837+ nil t)))
1.2838+
1.2839+(defun eplot--reset-transient ()
1.2840+ (interactive)
1.2841+ (with-current-buffer (or eplot--data-buffer (current-buffer))
1.2842+ (setq-local eplot--transient-settings nil)
1.2843+ (eplot-update-view-buffer)))
1.2844+
1.2845+(defun eplot--save-transient (file)
1.2846+ (interactive "FSave parameters to file: ")
1.2847+ (when (and (file-exists-p file)
1.2848+ (not (yes-or-no-p "File exists; overwrite? ")))
1.2849+ (user-error "Exiting"))
1.2850+ (let ((settings (with-current-buffer (or eplot--data-buffer (current-buffer))
1.2851+ eplot--transient-settings)))
1.2852+ (with-temp-buffer
1.2853+ (cl-loop for (name . value) in settings
1.2854+ do (insert (capitalize (symbol-name name)) ": "
1.2855+ (format "%s" value) "\n"))
1.2856+ (write-region (point-min) (point-max) file))))
1.2857+
1.2858+(defvar-keymap eplot-control-mode-map
1.2859+ "RET" #'eplot-control-update
1.2860+ "TAB" #'eplot-control-next-field
1.2861+ "C-<tab>" #'eplot-control-next-field
1.2862+ "<backtab>" #'eplot-control-prev-field)
1.2863+
1.2864+(define-derived-mode eplot-control-mode special-mode "eplot control"
1.2865+ (setq-local completion-at-point-functions
1.2866+ (cons 'eplot--complete-control completion-at-point-functions))
1.2867+ (add-hook 'before-change-functions #'eplot--process-text-input-before nil t)
1.2868+ (add-hook 'after-change-functions #'eplot--process-text-value nil t)
1.2869+ (add-hook 'after-change-functions #'eplot--process-text-input nil t)
1.2870+ (setq-local nobreak-char-display nil)
1.2871+ (setq truncate-lines t))
1.2872+
1.2873+(defun eplot--complete-control ()
1.2874+ ;; Complete headers names.
1.2875+ (when-let* ((input (get-text-property (point) 'input))
1.2876+ (name (plist-get input :name))
1.2877+ (spec (cdr (assq name (append eplot--plot-headers
1.2878+ eplot--chart-headers))))
1.2879+ (start (plist-get input :start))
1.2880+ (end (- (plist-get input :end) 2))
1.2881+ (completion-ignore-case t))
1.2882+ (skip-chars-backward " " start)
1.2883+ (or
1.2884+ (and (eq (plist-get spec :type) 'symbol)
1.2885+ (lambda ()
1.2886+ (let ((valid (plist-get spec :valid)))
1.2887+ (completion-in-region
1.2888+ (save-excursion
1.2889+ (skip-chars-backward "^ " start)
1.2890+ (point))
1.2891+ end
1.2892+ (mapcar #'symbol-name valid))
1.2893+ 'completion-attempted)))
1.2894+ (and (string-match "color" (symbol-name name))
1.2895+ (lambda ()
1.2896+ (completion-in-region
1.2897+ (save-excursion
1.2898+ (skip-chars-backward "^ " start)
1.2899+ (point))
1.2900+ end eplot--colors)
1.2901+ 'completion-attempted))
1.2902+ (and (string-match "\\bfile\\b" (symbol-name name))
1.2903+ (lambda ()
1.2904+ (completion-in-region
1.2905+ (save-excursion
1.2906+ (skip-chars-backward "^ " start)
1.2907+ (point))
1.2908+ end (directory-files "."))
1.2909+ 'completion-attempted))
1.2910+ (and (string-match "\\bfont\\b" (symbol-name name))
1.2911+ (lambda ()
1.2912+ (completion-in-region
1.2913+ (save-excursion
1.2914+ (skip-chars-backward "^ " start)
1.2915+ (point))
1.2916+ end
1.2917+ (eplot--font-families))
1.2918+ 'completion-attempted)))))
1.2919+
1.2920+(defun eplot--read-font-family (prompt)
1.2921+ "Prompt for a font family, possibly offering autocomplete."
1.2922+ (let ((families (eplot--font-families)))
1.2923+ (if families
1.2924+ (completing-read prompt families)
1.2925+ (read-string prompt))))
1.2926+
1.2927+(defun eplot--font-families ()
1.2928+ (when (executable-find "fc-list")
1.2929+ (let ((fonts nil))
1.2930+ (with-temp-buffer
1.2931+ (call-process "fc-list" nil t nil ":" "family")
1.2932+ (goto-char (point-min))
1.2933+ (while (re-search-forward "^\\([^,\n]+\\)" nil t)
1.2934+ (push (downcase (match-string 1)) fonts)))
1.2935+ (seq-uniq (sort fonts #'string<)))))
1.2936+
1.2937+(defun eplot-control-next-input ()
1.2938+ "Go to the next input field."
1.2939+ (interactive)
1.2940+ (when-let ((match (text-property-search-forward 'input)))
1.2941+ (goto-char (prop-match-beginning match))))
1.2942+
1.2943+(defun eplot-control-update ()
1.2944+ "Update the chart based on the current settings."
1.2945+ (interactive)
1.2946+ (let ((settings nil))
1.2947+ (save-excursion
1.2948+ (goto-char (point-min))
1.2949+ (while-let ((match (text-property-search-forward 'input)))
1.2950+ (when (equal (get-text-property (prop-match-beginning match) 'face)
1.2951+ 'eplot--input-changed)
1.2952+ (let* ((name (plist-get (prop-match-value match) :name))
1.2953+ (spec (cdr (assq name (append eplot--plot-headers
1.2954+ eplot--chart-headers))))
1.2955+ (value
1.2956+ (or (plist-get (prop-match-value match) :value)
1.2957+ (plist-get (prop-match-value match) :original-value))))
1.2958+ (setq value (string-trim (string-replace "\u00A0" " " value)))
1.2959+ (push (cons name
1.2960+ (cl-case (plist-get spec :type)
1.2961+ (number
1.2962+ (string-to-number value))
1.2963+ (symbol
1.2964+ (intern (downcase value)))
1.2965+ (symbol-list
1.2966+ (mapcar #'intern (split-string (downcase value))))
1.2967+ (t
1.2968+ value)))
1.2969+ settings)))))
1.2970+ (with-current-buffer eplot--data-buffer
1.2971+ (setq-local eplot--transient-settings (nreverse settings))
1.2972+ (eplot-update-view-buffer))))
1.2973+
1.2974+(defvar eplot--column-width nil)
1.2975+
1.2976+(defun eplot-create-controls ()
1.2977+ "Pop to a buffer that lists all parameters and allows editing."
1.2978+ (interactive)
1.2979+ (with-current-buffer (or eplot--data-buffer (current-buffer))
1.2980+ (let ((settings eplot--transient-settings)
1.2981+ (data-buffer (current-buffer))
1.2982+ (chart eplot--current-chart)
1.2983+ ;; Find the max width of all the different names.
1.2984+ (width (seq-max
1.2985+ (mapcar (lambda (e)
1.2986+ (length (cadr e)))
1.2987+ (apply #'append
1.2988+ (mapcar #'cdr
1.2989+ (apply #'append eplot--transients))))))
1.2990+ (transients (mapcar #'copy-sequence
1.2991+ (copy-sequence eplot--transients))))
1.2992+ (unless chart
1.2993+ (user-error "Must be called from an eplot buffer that has rendered a chart"))
1.2994+ ;; Rearrange the transients a bit for better display.
1.2995+ (let ((size (caar transients)))
1.2996+ (setcar (car transients) (caadr transients))
1.2997+ (setcar (cadr transients) size))
1.2998+ (pop-to-buffer "*eplot controls*")
1.2999+ (unless (eq major-mode 'eplot-control-mode)
1.3000+ (eplot-control-mode))
1.3001+ (setq-local eplot--data-buffer data-buffer
1.3002+ eplot--column-width (+ width 12 2))
1.3003+ (let ((inhibit-read-only t)
1.3004+ (before-change-functions nil)
1.3005+ (after-change-functions nil))
1.3006+ (erase-buffer)
1.3007+ (cl-loop for column in transients
1.3008+ for cn from 0
1.3009+ do
1.3010+ (goto-char (point-min))
1.3011+ (end-of-line)
1.3012+ (cl-loop
1.3013+ for row in column
1.3014+ do
1.3015+ (if (zerop cn)
1.3016+ (when (not (bobp))
1.3017+ (insert (format (format "%%-%ds" (+ width 14)) "")
1.3018+ "\n"))
1.3019+ (unless (= (count-lines (point-min) (point)) 1)
1.3020+ (if (eobp)
1.3021+ (progn
1.3022+ (insert (format (format "%%-%ds" (+ width 14)) "")
1.3023+ "\n")
1.3024+ (insert (format (format "%%-%ds" (+ width 14)) "")
1.3025+ "\n")
1.3026+ (forward-line -1)
1.3027+ (end-of-line))
1.3028+ (forward-line 1)
1.3029+ (end-of-line))))
1.3030+ ;; If we have a too-long input in the first column,
1.3031+ ;; then go to the next line.
1.3032+ (when (and (> cn 0)
1.3033+ (> (- (point) (pos-bol))
1.3034+ (+ width 12 2)))
1.3035+ (forward-line 1)
1.3036+ (end-of-line))
1.3037+ (insert (format (format "%%-%ds" (+ width 14))
1.3038+ (propertize (pop row) 'face 'bold)))
1.3039+ (if (looking-at "\n")
1.3040+ (forward-line 1)
1.3041+ (insert "\n"))
1.3042+ (cl-loop
1.3043+ for elem in row
1.3044+ for name = (cadr elem)
1.3045+ for slot = (intern (downcase name))
1.3046+ when (null (nth 2 elem))
1.3047+ do
1.3048+ (let* ((object (if (assq slot eplot--chart-headers)
1.3049+ chart
1.3050+ (car (slot-value chart 'plots))))
1.3051+ (value (format "%s"
1.3052+ (or (cdr (assq slot settings))
1.3053+ (if (not (slot-boundp object slot))
1.3054+ ""
1.3055+ (or (slot-value object slot)
1.3056+ ""))))))
1.3057+ (end-of-line)
1.3058+ ;; If we have a too-long input in the first column,
1.3059+ ;; then go to the next line.
1.3060+ (when (and (> cn 0)
1.3061+ (> (- (point) (pos-bol))
1.3062+ (+ width 12 2)))
1.3063+ (forward-line 1)
1.3064+ (end-of-line))
1.3065+ (when (and (> cn 0)
1.3066+ (bolp))
1.3067+ (insert (format (format "%%-%ds" (+ width 14)) "") "\n")
1.3068+ (forward-line -1)
1.3069+ (end-of-line))
1.3070+ (insert (format (format "%%-%ds" (1+ width)) name))
1.3071+ (eplot--input slot value
1.3072+ (if (cdr (assq slot settings))
1.3073+ 'eplot--input-changed
1.3074+ 'eplot--input-default))
1.3075+ (if (looking-at "\n")
1.3076+ (forward-line 1)
1.3077+ (insert "\n")))))))
1.3078+ (goto-char (point-min)))))
1.3079+
1.3080+(defface eplot--input-default
1.3081+ '((t :background "#505050"
1.3082+ :foreground "#a0a0a0"
1.3083+ :box (:line-width 1)))
1.3084+ "Face for eplot default inputs.")
1.3085+
1.3086+(defface eplot--input-changed
1.3087+ '((t :background "#505050"
1.3088+ :foreground "white"
1.3089+ :box (:line-width 1)))
1.3090+ "Face for eplot changed inputs.")
1.3091+
1.3092+(defvar-keymap eplot--input-map
1.3093+ :full t :parent text-mode-map
1.3094+ "RET" #'eplot-control-update
1.3095+ "TAB" #'eplot-input-complete
1.3096+ "C-a" #'eplot-move-beginning-of-input
1.3097+ "C-e" #'eplot-move-end-of-input
1.3098+ "C-k" #'eplot-kill-input
1.3099+ "C-<tab>" #'eplot-control-next-field
1.3100+ "<backtab>" #'eplot-control-prev-field)
1.3101+
1.3102+(defun eplot-input-complete ()
1.3103+ "Complete values in inputs."
1.3104+ (interactive)
1.3105+ (cond
1.3106+ ((let ((completion-fail-discreetly t))
1.3107+ (completion-at-point))
1.3108+ ;; Completion was performed; nothing else to do.
1.3109+ nil)
1.3110+ ((not (get-text-property (point) 'input))
1.3111+ (eplot-control-next-input))
1.3112+ (t
1.3113+ (user-error "No completion in this field"))))
1.3114+
1.3115+(defun eplot-move-beginning-of-input ()
1.3116+ "Move to the start of the current input field."
1.3117+ (interactive)
1.3118+ (if (= (point) (eplot--beginning-of-field))
1.3119+ (goto-char (pos-bol))
1.3120+ (goto-char (eplot--beginning-of-field))))
1.3121+
1.3122+(defun eplot-move-end-of-input ()
1.3123+ "Move to the end of the current input field."
1.3124+ (interactive)
1.3125+ (let ((input (get-text-property (point) 'input)))
1.3126+ (if (or (not input)
1.3127+ (= (point) (1- (plist-get input :end))))
1.3128+ (goto-char (pos-eol))
1.3129+ (goto-char (1+ (eplot--end-of-field))))))
1.3130+
1.3131+(defun eplot-control-next-field ()
1.3132+ "Move to the beginning of the next field."
1.3133+ (interactive)
1.3134+ (let ((input (get-text-property (point) 'input))
1.3135+ (start (point)))
1.3136+ (when input
1.3137+ (goto-char (plist-get input :end)))
1.3138+ (let ((match (text-property-search-forward 'input)))
1.3139+ (if match
1.3140+ (goto-char (prop-match-beginning match))
1.3141+ (goto-char start)
1.3142+ (user-error "No next field")))))
1.3143+
1.3144+(defun eplot-control-prev-field ()
1.3145+ "Move to the beginning of the previous field."
1.3146+ (interactive)
1.3147+ (let ((input (get-text-property (point) 'input))
1.3148+ (start (point)))
1.3149+ (when input
1.3150+ (goto-char (plist-get input :start))
1.3151+ (unless (bobp)
1.3152+ (forward-char -1)))
1.3153+ (let ((match (text-property-search-backward 'input)))
1.3154+ (unless match
1.3155+ (goto-char start)
1.3156+ (user-error "No previous field")))))
1.3157+
1.3158+(defun eplot-kill-input ()
1.3159+ "Remove the part of the input after point."
1.3160+ (interactive)
1.3161+ (let ((end (1+ (eplot--end-of-field))))
1.3162+ (kill-new (string-trim (buffer-substring (point) end)))
1.3163+ (delete-region (point) end)))
1.3164+
1.3165+(defun eplot--input (name value face)
1.3166+ (let ((start (point))
1.3167+ input)
1.3168+ (insert value)
1.3169+ (when (< (length value) 11)
1.3170+ (insert (make-string (- 11 (length value)) ?\u00A0)))
1.3171+ (put-text-property start (point) 'face face)
1.3172+ (put-text-property start (point) 'inhibit-read-only t)
1.3173+ (put-text-property start (point) 'input
1.3174+ (setq input
1.3175+ (list :name name
1.3176+ :size 11
1.3177+ :is-default (eq face 'eplot--input-default)
1.3178+ :original-value value
1.3179+ :original-face face
1.3180+ :start (set-marker (make-marker) start)
1.3181+ :value value)))
1.3182+ (put-text-property start (point) 'local-map eplot--input-map)
1.3183+ ;; This seems like a NOOP, but redoing the properties like this
1.3184+ ;; somehow makes `delete-region' work better.
1.3185+ (set-text-properties start (point) (text-properties-at start))
1.3186+ (insert (propertize " " 'face face
1.3187+ 'input input
1.3188+ 'inhibit-read-only t
1.3189+ 'local-map eplot--input-map))
1.3190+ (plist-put input :end (point-marker))
1.3191+ (insert " ")))
1.3192+
1.3193+(defun eplot--end-of-field ()
1.3194+ (- (plist-get (get-text-property (point) 'input) :end) 2))
1.3195+
1.3196+(defun eplot--beginning-of-field ()
1.3197+ (plist-get (get-text-property (point) 'input) :start))
1.3198+
1.3199+(defvar eplot--prev-deletion nil)
1.3200+
1.3201+(defun eplot--process-text-input-before (beg end)
1.3202+ (message "Before: %s %s" beg end)
1.3203+ (cond
1.3204+ ((= beg end)
1.3205+ (setq eplot--prev-deletion nil))
1.3206+ ((> end beg)
1.3207+ (setq eplot--prev-deletion (buffer-substring beg end)))))
1.3208+
1.3209+(defun eplot--process-text-input (beg end _replace-length)
1.3210+ ;;(message "After: %s %s %s %s" beg end replace-length eplot--prev-deletion)
1.3211+ (when-let ((props (if eplot--prev-deletion
1.3212+ (text-properties-at 0 eplot--prev-deletion)
1.3213+ (if (get-text-property end 'input)
1.3214+ (text-properties-at end)
1.3215+ (text-properties-at beg))))
1.3216+ (input (plist-get props 'input)))
1.3217+ ;; The action concerns something in the input field.
1.3218+ (let ((buffer-undo-list t)
1.3219+ (inhibit-read-only t)
1.3220+ (size (plist-get input :size)))
1.3221+ (save-excursion
1.3222+ (set-text-properties beg (- (plist-get input :end) 2) props)
1.3223+ (goto-char (1- (plist-get input :end)))
1.3224+ (let* ((remains (- (point) (plist-get input :start) 1))
1.3225+ (trim (- size remains 1)))
1.3226+ (if (< remains size)
1.3227+ ;; We need to add some padding.
1.3228+ (insert (apply #'propertize (make-string trim ?\u00A0)
1.3229+ props))
1.3230+ ;; We need to delete some padding, but only delete
1.3231+ ;; spaces at the end.
1.3232+ (setq trim (abs trim))
1.3233+ (while (and (> trim 0)
1.3234+ (eql (char-after (1- (point))) ?\u00A0))
1.3235+ (delete-region (1- (point)) (point))
1.3236+ (cl-decf trim))
1.3237+ (when (> trim 0)
1.3238+ (eplot--possibly-open-column)))))
1.3239+ ;; We re-set the properties so that they are continguous. This
1.3240+ ;; somehow makes the machinery that decides whether we can kill
1.3241+ ;; a word work better.
1.3242+ (set-text-properties (plist-get input :start)
1.3243+ (1- (plist-get input :end)) props)
1.3244+ ;; Compute what the value is now.
1.3245+ (let ((value (buffer-substring-no-properties
1.3246+ (plist-get input :start)
1.3247+ (plist-get input :end))))
1.3248+ (when (string-match "\u00A0+\\'" value)
1.3249+ (setq value (substring value 0 (match-beginning 0))))
1.3250+ (plist-put input :value value)))))
1.3251+
1.3252+(defun eplot--possibly-open-column ()
1.3253+ (save-excursion
1.3254+ (when-let ((input (get-text-property (point) 'input)))
1.3255+ (goto-char (plist-get input :end)))
1.3256+ (unless (looking-at " *\n")
1.3257+ (skip-chars-forward " ")
1.3258+ (while (not (eobp))
1.3259+ (let ((text (buffer-substring (point) (pos-eol))))
1.3260+ (delete-region (point) (pos-eol))
1.3261+ (forward-line 1)
1.3262+ (if (eobp)
1.3263+ (insert (make-string eplot--column-width ?\s) text "\n")
1.3264+ (forward-char eplot--column-width)
1.3265+ (if (get-text-property (point) 'input)
1.3266+ (forward-line 1)
1.3267+ (insert text)
1.3268+ ;; We have to fix up the markers.
1.3269+ (save-excursion
1.3270+ (let* ((match (text-property-search-backward 'input))
1.3271+ (input (prop-match-value match)))
1.3272+ (plist-put input :start
1.3273+ (set-marker (plist-get input :start)
1.3274+ (prop-match-beginning match)))
1.3275+ (plist-put input :end
1.3276+ (set-marker (plist-get input :end)
1.3277+ (+ (prop-match-end match) 1))))))))))))
1.3278+
1.3279+(defun eplot--process-text-value (beg _end _replace-length)
1.3280+ (when-let* ((input (get-text-property beg 'input)))
1.3281+ (let ((inhibit-read-only t))
1.3282+ (when (plist-get input :is-default)
1.3283+ (put-text-property (plist-get input :start)
1.3284+ (plist-get input :end)
1.3285+ 'face
1.3286+ (if (equal (plist-get input :original-value)
1.3287+ (plist-get input :value))
1.3288+ 'eplot--input-default
1.3289+ 'eplot--input-changed))))))
1.3290+
1.3291+(defun eplot--read-color (prompt)
1.3292+ "Read an SVG color."
1.3293+ (completing-read prompt eplot--colors))
1.3294+
1.3295+(eval `(transient-define-prefix eplot-customize ()
1.3296+ "Customize Chart"
1.3297+ ,@(eplot--define-transients)))
1.3298+
1.3299+(defun eplot--bezier (factor i points)
1.3300+ (cl-labels ((padd (p1 p2)
1.3301+ (cons (+ (car p1) (car p2)) (+ (cdr p1) (cdr p2))))
1.3302+ (psub (p1 p2)
1.3303+ (cons (- (car p1) (car p2)) (- (cdr p1) (cdr p2))))
1.3304+ (pscale (factor point)
1.3305+ (cons (* factor (car point)) (* factor (cdr point)))))
1.3306+ (let* ((start (elt points (1- i)))
1.3307+ (end (elt points i))
1.3308+ (prev (if (< (- i 2) 0)
1.3309+ start
1.3310+ (elt points (- i 2))))
1.3311+ (next (if (> (1+ i) (1- (length points)))
1.3312+ end
1.3313+ (elt points (1+ i))))
1.3314+ (start-control-point
1.3315+ (padd start (pscale factor (psub end prev))))
1.3316+ (end-control-point
1.3317+ (padd end (pscale factor (psub start next)))))
1.3318+ (list (car start-control-point)
1.3319+ (cdr start-control-point)
1.3320+ (car end-control-point)
1.3321+ (cdr end-control-point)
1.3322+ (car end)
1.3323+ (cdr end)))))
1.3324+
1.3325+;;; CSV Parsing Stuff.
1.3326+
1.3327+(defun eplot--csv-buffer-p ()
1.3328+ (save-excursion
1.3329+ (goto-char (point-min))
1.3330+ (let ((min 1.0e+INF)
1.3331+ (max -1.0e+INF)
1.3332+ (total 0)
1.3333+ (lines 0))
1.3334+ (while (not (eobp))
1.3335+ (let ((this 0))
1.3336+ (while (search-forward "," (pos-eol) t)
1.3337+ (cl-incf total)
1.3338+ (cl-incf this))
1.3339+ (forward-line 1)
1.3340+ (cl-incf lines)
1.3341+ (setq min (min min this)
1.3342+ max (max max this))))
1.3343+ (let ((mid (e/ total lines)))
1.3344+ ;; If we have a comma on each line, and it's fairly evenly
1.3345+ ;; distributed, it's a CSV buffer.
1.3346+ (and (>= min 1)
1.3347+ (< (* mid 0.9) min)
1.3348+ (> (* mid 1.1) max))))))
1.3349+
1.3350+(defun eplot--numericalp (value)
1.3351+ (string-match-p "\\`[-.0-9]*\\'" value))
1.3352+
1.3353+(defun eplot--numberish (value)
1.3354+ (if (or (zerop (length value))
1.3355+ (not (eplot--numericalp value)))
1.3356+ value
1.3357+ (string-to-number value)))
1.3358+
1.3359+(defun eplot--parse-csv-buffer ()
1.3360+ (unless (fboundp 'pcsv-parse-buffer)
1.3361+ (user-error "You need to install the pcsv package to parse CSV files"))
1.3362+ (let ((csv (and (fboundp 'pcsv-parse-buffer)
1.3363+ ;; This repeated check is just to silence the byte
1.3364+ ;; compiler.
1.3365+ (pcsv-parse-buffer)))
1.3366+ names)
1.3367+ ;; Check whether the first line looks like a header.
1.3368+ (when (and (length> csv 1)
1.3369+ ;; The second line is all numbers...
1.3370+ (cl-every #'eplot--numericalp (nth 1 csv))
1.3371+ ;; .. and the first line isn't.
1.3372+ (not (cl-every #'eplot--numericalp (nth 0 csv))))
1.3373+ (setq names (pop csv)))
1.3374+ (list
1.3375+ (cons 'legend (and names "true"))
1.3376+ (cons :plots
1.3377+ (cl-loop
1.3378+ for column from 1 upto (1- (length (car csv)))
1.3379+ collect
1.3380+ (list (cons :headers
1.3381+ (list
1.3382+ (cons 'name (elt names column))
1.3383+ (cons 'data-format
1.3384+ (cond
1.3385+ ((cl-every (lambda (e) (<= (length e) 4))
1.3386+ (mapcar #'car csv))
1.3387+ "year")
1.3388+ ((cl-every (lambda (e) (= (length e) 8))
1.3389+ (mapcar #'car csv))
1.3390+ "date")
1.3391+ (t
1.3392+ "number")))
1.3393+ (cons 'color (eplot--vary-color "vary" (1- column)))))
1.3394+ (cons
1.3395+ :values
1.3396+ (cl-loop for line in csv
1.3397+ collect (list :x (eplot--numberish (car line))
1.3398+ :value (eplot--numberish
1.3399+ (elt line column)))))))))))
1.3400+
1.3401+(declare-function org-element-parse-buffer "org-element")
1.3402+
1.3403+(defun eplot--parse-org-buffer ()
1.3404+ (require 'org-element)
1.3405+ (let* ((table (nth 2 (nth 2 (org-element-parse-buffer))))
1.3406+ (columns (cl-loop for cell in (nthcdr 2 (nth 2 table))
1.3407+ collect (substring-no-properties (nth 2 cell))))
1.3408+ (value-column (or (seq-position columns "value") 0))
1.3409+ (date-column (seq-position columns "date")))
1.3410+ `((:plots
1.3411+ ((:headers
1.3412+ ,@(and date-column '((data-format . "date"))))
1.3413+ (:values
1.3414+ ,@(cl-loop for row in (nthcdr 4 table)
1.3415+ collect
1.3416+ (let ((cells (cl-loop for cell in (nthcdr 2 row)
1.3417+ collect (substring-no-properties
1.3418+ (nth 2 cell)))))
1.3419+ (list :value (string-to-number (elt cells value-column))
1.3420+ :x (string-to-number
1.3421+ (replace-regexp-in-string
1.3422+ "[^0-9]" "" (elt cells date-column)))
1.3423+ )))))))))
1.3424+
1.3425+(provide 'eplot)
1.3426+
1.3427+;;; eplot.el ends here