diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-11 13:14:56 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-11 13:14:56 -0500 |
| commit | d9897243c88b39109996e67f214ebff63d000742 (patch) | |
| tree | 3a84a6dd7b8f3e540ed5da2ad2c86872b5c734a3 /custom/eplot.el | |
| parent | 559530a208f8287d450cd842bfbdda92da97971c (diff) | |
| download | dotemacs-d9897243c88b39109996e67f214ebff63d000742.tar.gz dotemacs-d9897243c88b39109996e67f214ebff63d000742.zip | |
refactor: retire unreferenced modules to an archive directory
A reachability sweep from init.el found eight .el files that nothing in the config loads or references. I moved them under archive/ (off the load-path, still tracked in git) so they stay readable and restorable rather than deleted. archive/README.org records the rule (every .el outside archive/ should be actively used) and why each file was retired.
I retired show-kill-ring, duet-config, and mu4e-org-contacts-setup from modules/, and eplot, profile-dotemacs, titlecase, titlecase-data, and edit-indirect from custom/. Retiring show-kill-ring also removed the orphaned M-K -> M-S-k translation in keyboard-compat, which had left M-K a dead key. Its tests now assert seventeen translations and guard M-K's absence.
Diffstat (limited to 'custom/eplot.el')
| -rw-r--r-- | custom/eplot.el | 3495 |
1 files changed, 0 insertions, 3495 deletions
diff --git a/custom/eplot.el b/custom/eplot.el deleted file mode 100644 index c00a3e31..00000000 --- a/custom/eplot.el +++ /dev/null @@ -1,3495 +0,0 @@ -;;; eplot.el --- Manage and Edit Wordpress Posts -*- lexical-binding: t -*- - -;; Copyright (C) 2024 Free Software Foundation, Inc. - -;; Author: Lars Magne Ingebrigtsen <larsi@gnus.org> -;; Keywords: charts -;; Package: eplot -;; Version: 1.0 -;; Package-Requires: ((emacs "29.0.59") (pcsv "0.0")) - -;; eplot is free software; you can redistribute it and/or modify it -;; under the terms of the GNU General Public License as published by -;; the Free Software Foundation; either version 2, or (at your option) -;; any later version. - -;;; Commentary: - -;; The main entry point is `M-x eplot' in a buffer with time series -;; data. -;; -;; If installing manually, put something like the following in your -;; Emacs init file (but adjust the path to where you've put eplot): -;; -;; (push "~/src/eplot/" load-path) -;; (autoload 'eplot "eplot" nil t) -;; (autoload 'eplot-mode "eplot" nil t) -;; (unless (assoc "\\.plt" auto-mode-alist) -;; (setq auto-mode-alist (cons '("\\.plt" . eplot-mode) auto-mode-alist))) - -;; This requires the pcsv package to parse CSV files. - -;;; Code: - -(require 'svg) -(require 'cl-lib) -(require 'face-remap) -(require 'eieio) -(require 'iso8601) -(require 'transient) - -(defvar eplot--user-defaults nil) -(defvar eplot--chart-headers nil) -(defvar eplot--plot-headers nil) -(defvar eplot--transient-settings nil) - - -(defvar eplot--colors - '("aliceblue" "antiquewhite" "aqua" "aquamarine" "azure" "beige" "bisque" - "black" "blanchedalmond" "blue" "blueviolet" "brown" "burlywood" - "cadetblue" "chartreuse" "chocolate" "coral" "cornflowerblue" "cornsilk" - "crimson" "cyan" "darkblue" "darkcyan" "darkgoldenrod" "darkgray" - "darkgreen" "darkgrey" "darkkhaki" "darkmagenta" "darkolivegreen" - "darkorange" "darkorchid" "darkred" "darksalmon" "darkseagreen" - "darkslateblue" "darkslategray" "darkslategrey" "darkturquoise" - "darkviolet" "deeppink" "deepskyblue" "dimgray" "dimgrey" "dodgerblue" - "firebrick" "floralwhite" "forestgreen" "fuchsia" "gainsboro" "ghostwhite" - "gold" "goldenrod" "gray" "green" "greenyellow" "grey" "honeydew" "hotpink" - "indianred" "indigo" "ivory" "khaki" "lavender" "lavenderblush" "lawngreen" - "lemonchiffon" "lightblue" "lightcoral" "lightcyan" "lightgoldenrodyellow" - "lightgray" "lightgreen" "lightgrey" "lightpink" "lightsalmon" - "lightseagreen" "lightskyblue" "lightslategray" "lightslategrey" - "lightsteelblue" "lightyellow" "lime" "limegreen" "linen" "magenta" - "maroon" "mediumaquamarine" "mediumblue" "mediumorchid" "mediumpurple" - "mediumseagreen" "mediumslateblue" "mediumspringgreen" "mediumturquoise" - "mediumvioletred" "midnightblue" "mintcream" "mistyrose" "moccasin" - "navajowhite" "navy" "oldlace" "olive" "olivedrab" "orange" "orangered" - "orchid" "palegoldenrod" "palegreen" "paleturquoise" "palevioletred" - "papayawhip" "peachpuff" "peru" "pink" "plum" "powderblue" "purple" "red" - "rosybrown" "royalblue" "saddlebrown" "salmon" "sandybrown" "seagreen" - "seashell" "sienna" "silver" "skyblue" "slateblue" "slategray" "slategrey" - "snow" "springgreen" "steelblue" "tan" "teal" "thistle" "tomato" - "turquoise" "violet" "wheat" "white" "whitesmoke" "yellow" "yellowgreen")) - -(defun eplot-set (header value) - "Set the default value of HEADER to VALUE. -To get a list of all possible HEADERs, use the `M-x -eplot-list-chart-headers' command. - -Also see `eplot-reset'." - (let ((elem (or (assq header eplot--chart-headers) - (assq header eplot--plot-headers)))) - (unless elem - (error "No such header type: %s" header)) - (eplot--add-default header value))) - -(defun eplot--add-default (header value) - ;; We want to preserve the order defaults have been added, so that - ;; we can apply them in the same order. This makes a difference - ;; when we're dealing with specs that have inheritence. - (setq eplot--user-defaults (delq (assq header eplot--user-defaults) - eplot--user-defaults)) - (setq eplot--user-defaults (list (cons header value)))) - -(defun eplot-reset (&optional header) - "Reset HEADER to defaults. -If HEADER is nil or not present, reset everything to defaults." - (if header - (setq eplot--user-defaults (delq (assq header eplot--user-defaults) - eplot--user-defaults)) - (setq eplot--user-defaults nil))) - -(unless (assoc "\\.plt" auto-mode-alist) - (setq auto-mode-alist (cons '("\\.plt" . eplot-mode) auto-mode-alist))) - -;;; eplot modes. - -(defvar-keymap eplot-mode-map - "C-c C-c" #'eplot-update-view-buffer - "C-c C-p" #'eplot-switch-view-buffer - "C-c C-e" #'eplot-list-chart-headers - "C-c C-v" #'eplot-customize - "C-c C-l" #'eplot-create-controls - "TAB" #'eplot-complete) - -;; # is working overtime in the syntax here: -;; It can be a color like Color: #e0e0e0, and -;; it can be a setting like 33 # Label: Apples, -;; when it starts a line it's a comment. -(defvar eplot-font-lock-keywords - `(("^[ \t\n]*#.*" . font-lock-comment-face) - ("^[^ :\n]+:" . font-lock-keyword-face) - ("#[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) - ("#.*" . font-lock-builtin-face))) - -(define-derived-mode eplot-mode text-mode "eplot" - "Major mode for editing charts. -Use the \\[eplot-list-chart-headers] command to get a list of all -possible chart headers." - (setq-local completion-at-point-functions - (cons 'eplot--complete-header completion-at-point-functions)) - (setq-local font-lock-defaults - '(eplot-font-lock-keywords nil nil nil))) - -(defun eplot-complete () - "Complete headers." - (interactive) - (cond - ((let ((completion-fail-discreetly t)) - (completion-at-point)) - ;; Completion was performed; nothing else to do. - nil) - (t (indent-relative)))) - -(defun eplot--complete-header () - (or - ;; Complete headers names. - (and (or (looking-at ".*:") - (and (looking-at "[ \t]*$") - (save-excursion - (beginning-of-line) - (not (looking-at "\\(.+\\):"))))) - (lambda () - (let ((headers (mapcar - (lambda (h) - (if (looking-at ".*:") - (capitalize (symbol-name (car h))) - (concat (capitalize (symbol-name (car h))) ": "))) - (save-excursion - ;; If we're after the headers, then we want - ;; to complete over the plot headers. Otherwise, - ;; complete over the chart headers. - (if (and (not (bobp)) - (progn - (forward-line -1) - (re-search-backward "^[ \t]*$" nil t))) - eplot--plot-headers - eplot--chart-headers)))) - (completion-ignore-case t)) - (completion-in-region (pos-bol) (line-end-position) headers) - 'completion-attempted))) - ;; Complete header values. - (let ((hname nil)) - (and (save-excursion - (and (looking-at "[ \t]*$") - (progn - (beginning-of-line) - (and (looking-at "\\(.+\\):") - (setq hname (intern (downcase (match-string 1))))))) - (lambda () - (let ((valid (plist-get - (cdr (assq hname (append eplot--plot-headers - eplot--chart-headers))) - :valid)) - (completion-ignore-case t)) - (completion-in-region - (save-excursion - (search-backward ":" (pos-bol) t) - (skip-chars-forward ": \t") - (point)) - (line-end-position) - (mapcar #'symbol-name valid)) - 'completion-attempted))))))) - -(define-minor-mode eplot-minor-mode - "Minor mode to issue commands from an eplot data buffer." - :lighter " eplot") - -(defvar-keymap eplot-minor-mode-map - "H-l" #'eplot-eval-and-update) - -(defvar-keymap eplot-view-mode-map - "s" #'eplot-view-write-file - "w" #'eplot-view-write-scaled-file - "c" #'eplot-view-customize - "l" #'eplot-create-controls) - -(define-derived-mode eplot-view-mode special-mode "eplot view" - "Major mode for displaying eplots." - (setq-local revert-buffer-function #'eplot-update - cursor-type nil)) - -(defun eplot-view-write-file (file &optional width) - "Write the current chart to a file. -If you type in a file name that ends with something else than \"svg\", -ImageMagick \"convert\" will be used to convert the image first. - -If writing to a PNG file, \"rsvg-conver\" will be used instead if -it exists as this usually gives better results." - (interactive "FWrite to file name: ") - (when (and (file-exists-p file) - (not (yes-or-no-p "File exists, overwrite? "))) - (error "Not overwriting the file")) - (save-excursion - (goto-char (point-min)) - (let ((match - (text-property-search-forward 'display nil - (lambda (_ e) - (and (consp e) - (eq (car e) 'image)))))) - (unless match - (error "Can't find an image in the current buffer")) - (let ((svg (plist-get (cdr (prop-match-value match)) :data)) - (tmp " *eplot convert*") - (executable (if width "rsvg-convert" "convert")) - sfile ofile) - (unless svg - (error "Invalid image in the current buffer")) - (with-temp-buffer - (set-buffer-multibyte nil) - (svg-print svg) - (if (string-match-p "\\.svg\\'" file) - (write-region (point-min) (point-max) file) - (if (and (string-match-p "\\.png\\'" file) - (executable-find "rsvg-convert")) - (setq executable "rsvg-convert") - (unless (executable-find executable) - (error "%s isn't installed; can only save svg files" - executable))) - (when (and (equal executable "rsvg-convert") - (not (string-match-p "\\.png\\'" file)) - (not (executable-find "convert"))) - (error "Can only write PNG files when scaling because \"convert\" isn't installed")) - (unwind-protect - (progn - (setq sfile (make-temp-file "eplot" nil ".svg") - ofile (make-temp-file "eplot" nil ".png")) - (write-region (point-min) (point-max) sfile nil 'silent) - ;; We don't use `call-process-region', because - ;; convert doesn't seem to like that? - (let ((code (if (equal executable "rsvg-convert") - (apply - #'call-process - executable nil (get-buffer-create tmp) nil - `(,(format "--output=%s" - (expand-file-name ofile)) - ,@(and width - `(,(format "--width=%d" width) - "--keep-aspect-ratio")) - ,sfile)) - (call-process - executable nil (get-buffer-create tmp) nil - sfile file)))) - (eplot--view-error code tmp) - (when (file-exists-p ofile) - (if (string-match-p "\\.png\\'" file) - (rename-file ofile file) - (let ((code (call-process "convert" nil tmp nil - ofile file))) - (eplot--view-error code tmp)))) - (message "Wrote %s" file))) - ;; Clean-up. - (when (get-buffer tmp) - (kill-buffer tmp)) - (when (file-exists-p sfile) - (delete-file sfile)) - (when (file-exists-p ofile) - (delete-file sfile))))))))) - -(defun eplot--view-error (code tmp) - (unless (zerop code) - (error "Error code %d: %s" - code - (with-current-buffer tmp - (while (search-forward "[ \t\n]+" nil t) - (replace-match " ")) - (string-trim (buffer-string)))))) - -(defun eplot-view-write-scaled-file (width file) - "Write the current chart to a rescaled to a file. -The rescaling is done by \"rsvg-convert\", which has to be -installed. Rescaling is done when rendering, so this should give -you a clear, non-blurry version of the chart at any size." - (interactive "nWidth: \nFWrite to file: ") - (eplot-view-write-file file width)) - -(defun eplot-view-customize () - "Customize the settings for the chart in the current buffer." - (interactive) - (with-suppressed-warnings ((interactive-only eplot-customize)) - (eplot-customize))) - -(defvar eplot--data-buffer nil) -(defvar eplot--current-chart nil) - -(defun eplot () - "Plot the data in the current buffer." - (interactive) - (eplot-update-view-buffer)) - -(defun eplot-with-headers (header-file) - "Plot the data in the current buffer using headers from a file." - (interactive "fHeader file: ") - (eplot-update-view-buffer - (with-temp-buffer - (insert-file-contents header-file) - (eplot--parse-headers)))) - -(defun eplot-make-plot (headers &rest datas) - "Return an SVG based on DATA. -DATA should be pairs of headers, then followed by the plot data." - (with-temp-buffer - ;; Insert the headers. - (dolist (line headers) - (insert (format "%s:" (pop line))) - (dolist (elem line) - (insert (format " %s" elem))) - (insert "\n")) - ;; Then insert all the plot data sets. - (dolist (data datas) - (insert "\n") - (dolist (line data) - (dolist (elem line) - (insert (format "%s" elem) " ")) - (insert "\n"))) - ;;(setq a (buffer-string)) (debug) - (eplot--render (eplot--parse-buffer) t))) - -(defun eplot-switch-view-buffer () - "Switch to the eplot view buffer and render the chart." - (interactive) - (eplot-update-view-buffer nil t)) - -(defun eplot-update-view-buffer (&optional headers switch) - "Update the eplot view buffer based on the current data buffer." - (interactive) - ;; This is mainly useful during implementation. - (if (and (eq major-mode 'emacs-lisp-mode) - (get-buffer-window "*eplot*" t)) - (with-current-buffer "*eplot*" - (eplot-update) - (when-let ((win (get-buffer-window "*eplot*" t))) - (set-window-point win (point-min)))) - ;; Normal case. - (let* ((eplot--user-defaults (eplot--settings-table)) - (data (eplot--parse-buffer)) - (data-buffer (current-buffer)) - (window (selected-window))) - (unless data - (user-error "No data in the current buffer")) - (setq data (eplot--inject-headers data headers)) - (if (get-buffer-window "*eplot*" t) - (set-buffer "*eplot*") - (if switch - (pop-to-buffer-same-window "*eplot*") - (pop-to-buffer "*eplot*"))) - (let ((inhibit-read-only t)) - (erase-buffer) - (unless (eq major-mode 'eplot-view-mode) - (eplot-view-mode)) - (setq-local eplot--data-buffer data-buffer) - (let ((chart (eplot--render data))) - (with-current-buffer data-buffer - (setq-local eplot--current-chart chart))) - (insert "\n") - (when-let ((win (get-buffer-window "*eplot*" t))) - (set-window-point win (point-min)))) - (select-window window)))) - -(defun eplot--settings-table () - (if (not eplot--transient-settings) - eplot--user-defaults - (append eplot--user-defaults eplot--transient-settings))) - -(defun eplot--inject-headers (data headers) - ;; It's OK not to separate the plot headers from the chart - ;; headers. Collect them here, if any. - (when-let ((plot-headers - (cl-loop for elem in (mapcar #'car eplot--plot-headers) - for value = (eplot--vs elem headers) - when value - collect (progn - ;; Remove these headers from the data - ;; headers so that we don't get errors - ;; on undefined headers. - (setq headers (delq (assq elem headers) - headers)) - (cons elem value))))) - (dolist (plot (cdr (assq :plots data))) - (let ((headers (assq :headers plot))) - (if headers - (nconc headers plot-headers) - (nconc plot (list (list :headers plot-headers))))))) - (append data headers)) - -(defun eplot-eval-and-update () - "Helper command when developing." - (interactive nil emacs-lisp-mode) - (save-some-buffers t) - (elisp-eval-buffer) - (eval-defun nil) - (eplot-update-view-buffer)) - -;;; Parsing buffers. - -(defun eplot-update (&rest _ignore) - "Update the plot in the current buffer." - (interactive) - (unless eplot--data-buffer - (user-error "No data buffer associated with this eplot view buffer")) - (let ((data (with-current-buffer eplot--data-buffer - (eplot--parse-buffer))) - (eplot--user-defaults (with-current-buffer eplot--data-buffer - (eplot--settings-table))) - (inhibit-read-only t)) - (erase-buffer) - (let ((chart (eplot--render data))) - (with-current-buffer eplot--data-buffer - (setq-local eplot--current-chart chart))) - (insert "\n\n"))) - -(defun eplot--parse-buffer () - (if (eq major-mode 'org-mode) - (eplot--parse-org-buffer) - (eplot--parse-eplot-buffer))) - -(defun eplot--parse-eplot-buffer () - (if (eplot--csv-buffer-p) - (eplot--parse-csv-buffer) - (let ((buf (current-buffer))) - (with-temp-buffer - (insert-buffer-substring buf) - ;; Remove comments first. - (goto-char (point-min)) - (while (re-search-forward "^[ \t]*#" nil t) - (delete-line)) - (goto-char (point-min)) - ;; First headers. - (let* ((data (eplot--parse-headers)) - (plot-headers - ;; It's OK not to separate the plot headers from the chart - ;; headers. Collect them here, if any. - (cl-loop for elem in (mapcar #'car eplot--plot-headers) - for value = (eplot--vs elem data) - when value - collect (progn - ;; Remove these headers from the data - ;; headers so that we don't get errors - ;; on undefined headers. - (setq data (delq (assq elem data) data)) - (cons elem value)))) - plots) - ;; Then the values. - (while-let ((plot (eplot--parse-values nil plot-headers))) - (setq plot-headers nil) - (push plot plots)) - (when plots - (push (cons :plots (nreverse plots)) data)) - data))))) - -(defun eplot--parse-headers () - (let ((data nil) - type value) - (while (looking-at "\\([^\n\t :]+\\):\\(.*\\)") - (setq type (intern (downcase (match-string 1))) - value (substring-no-properties (string-trim (match-string 2)))) - (forward-line 1) - ;; Get continuation lines. - (while (looking-at "[ \t]+\\(.*\\)") - (setq value (concat value " " (string-trim (match-string 1)))) - (forward-line 1)) - (if (eq type 'header-file) - (setq data (nconc data - (with-temp-buffer - (insert-file-contents value) - (eplot--parse-headers)))) - ;; We don't use `push' here because we want to preserve order - ;; also when inserting headers from other files. - (setq data (nconc data (list (cons type value)))))) - data)) - -(defun eplot--parse-values (&optional in-headers data-headers) - ;; Skip past separator lines. - (while (looking-at "[ \t]*\n") - (forward-line 1)) - (let* ((values nil) - ;; We may have plot-specific headers. - (headers (nconc (eplot--parse-headers) data-headers)) - (data-format (or (eplot--vyl 'data-format headers) - (eplot--vyl 'data-format in-headers))) - (two-values (memq 'two-values data-format)) - (xy (or (memq 'year data-format) - (memq 'date data-format) - (memq 'time data-format) - (memq 'xy data-format))) - (data-column (or (eplot--vn 'data-column headers) - (eplot--vn 'data-column in-headers)))) - (if-let ((data-file (eplot--vs 'data-file headers))) - (with-temp-buffer - (insert-file-contents data-file) - (setq values (cdr (assq :values (eplot--parse-values headers))) - headers (delq (assq 'data headers) headers))) - ;; Now we come to the data. The data is typically either just a - ;; number, or two numbers (in which case the first number is a - ;; date or a time). Labels ans settings can be introduced with - ;; a # char. - (while (looking-at "\\([-0-9. \t]+\\)\\([ \t]*#\\(.*\\)\\)?") - (let ((numbers (match-string 1)) - (settings (eplot--parse-settings (match-string 3))) - this) - (setq numbers (mapcar #'string-to-number - (split-string (string-trim numbers)))) - ;; If we're reading two dimensionalish data, the first - ;; number is the date/time/x. - (when xy - (setq this (list :x (pop numbers)))) - ;; Chop off all the numbers until we read the column(s) - ;; we're using. - (when data-column - (setq numbers (nthcdr (1- data-column) numbers))) - (when numbers - (setq this (nconc this (list :value (pop numbers))))) - (when two-values - (setq this (nconc this (list :extra-value (pop numbers))))) - (when settings - (setq this (nconc this (list :settings settings)))) - (when (plist-get this :value) - (push this values))) - (forward-line 1)) - (setq values (nreverse values))) - (and values - `((:headers . ,headers) (:values . ,values))))) - -(defun eplot--parse-settings (string) - (when string - (with-temp-buffer - (insert (string-trim string) "\n") - (goto-char (point-min)) - (while (re-search-forward "\\(.\\)," nil t) - (if (equal (match-string 1) "\\") - (replace-match "," t t) - (delete-char -1) - (insert "\n") - (when (looking-at "[ \t]+") - (replace-match "")))) - (goto-char (point-min)) - (eplot--parse-headers)))) - -;;; Accessing data. - -(defun eplot--vn (type data &optional default) - (if-let ((value (cdr (assq type data)))) - (string-to-number value) - default)) - -(defun eplot--vs (type data &optional default) - (or (cdr (assq type data)) default)) - -(defun eplot--vy (type data &optional default) - (if-let ((value (cdr (assq type data)))) - (intern (downcase value)) - default)) - -(defun eplot--vyl (type data &optional default) - (if-let ((value (cdr (assq type data)))) - (mapcar #'intern (split-string (downcase value))) - default)) - -(defmacro eplot-def (args doc-string) - (declare (indent defun)) - `(eplot--def ',(nth 0 args) ',(nth 1 args) ',(nth 2 args) ',(nth 3 args) - ,doc-string)) - -(defun eplot--def (name type default valid doc) - (setq eplot--chart-headers (delq (assq name eplot--chart-headers) - eplot--chart-headers)) - (push (list name - :type type - :default default - :doc doc - :valid valid) - eplot--chart-headers)) - -(eplot-def (width number) - "The width of the entire chart.") - -(eplot-def (height number) - "The height of the entire chart.") - -(eplot-def (format symbol normal (normal bar-chart horizontal-bar-chart)) - "The overall format of the chart.") - -(eplot-def (layout symbol nil (normal compact)) - "The general layout of the chart.") - -(eplot-def (mode symbol light (dark light)) - "Dark/light mode.") - -(eplot-def (margin-left number 70) - "The left margin.") - -(eplot-def (margin-right number 20) - "The right margin.") - -(eplot-def (margin-top number 40) - "The top margin.") - -(eplot-def (margin-bottom number 60) - "The bottom margin.") - -(eplot-def (x-axis-title-space number 5) - "The space between the X axis and the label.") - -(eplot-def (font string "sans-serif") - "The font to use in titles, labels and legends.") - -(eplot-def (font-size number 12) - "The font size.") - -(eplot-def (font-weight symbol bold (bold normal)) - "The font weight.") - -(eplot-def (label-font string (spec font)) - "The font to use for axes labels.") - -(eplot-def (label-font-size number (spec font-size)) - "The font size to use for axes labels.") - -(eplot-def (horizontal-label-font-size number (spec label-font-size)) - "The font size to use for horizontal labels.") - -(eplot-def (bar-font string (spec font)) - "The font to use for bar chart labels.") - -(eplot-def (bar-font-size number (spec font-size)) - "The font size to use for bar chart labels.") - -(eplot-def (bar-font-weight symbol (spec font-weight) (bold normal)) - "The font weight to use for bar chart labels.") - -(eplot-def (chart-color string "black") - "The foreground color to use in plots, axes, legends, etc. -This is used as the default, but can be overridden per thing.") - -(eplot-def (background-color string "white") - "The background color. -If you want a chart with a transparent background, use the color -\"none\".") - -(eplot-def (background-gradient string) - "Use this to get a gradient color in the background.") - -(eplot-def (axes-color string (spec chart-color)) - "The color of the axes.") - -(eplot-def (grid-color string "#e0e0e0") - "The color of the grid.") - -(eplot-def (grid symbol xy (xy x y off)) - "What grid axes to do.") - -(eplot-def (grid-opacity number) - "The opacity of the grid. -This should either be nil or a value between 0 and 1, where 0 is -fully transparent.") - -(eplot-def (grid-position symbol bottom (bottom top)) - "Whether to put the grid on top or under the plot.") - -(eplot-def (legend symbol nil (true nil)) - "Whether to do a legend.") - -(eplot-def (legend-color string (spec chart-color)) - "The color of legends (if any).") - -(eplot-def (legend-border-color string (spec chart-color)) - "The border color of legends (if any).") - -(eplot-def (legend-background-color string (spec background-color)) - "The background color of legends (if any).") - -(eplot-def (label-color string (spec axes-color)) - "The color of labels on the axes.") - -(eplot-def (surround-color string) - "The color between the plot area and the edges of the chart.") - -(eplot-def (border-color string) - "The color of the border of the chart, if any.") - -(eplot-def (border-width number) - "The width of the border of the chart, if any.") - -(eplot-def (frame-color string) - "The color of the frame of the plot, if any.") - -(eplot-def (frame-width number) - "The width of the frame of the plot, if any.") - -(eplot-def (min number) - "The minimum value in the chart. -This is normally computed automatically, but can be overridden - with this spec.") - -(eplot-def (max number) - "The maximum value in the chart. -This is normally computed automatically, but can be overridden - with this spec.") - -(eplot-def (title string) - "The title of the chart, if any.") - -(eplot-def (title-color string (spec chart-color)) - "The color of the title.") - -(eplot-def (x-title string) - "The title of the X axis, if any.") - -(eplot-def (y-title string) - "The title of the X axis, if any.") - -(eplot-def (x-label-format string) - "Format string for the X labels. -This is a `format' string.") - -(eplot-def (y-label-format string) - "Format string for the Y labels. -This is a `format' string.") - -(eplot-def (horizontal-label-left number) - "Position of the horizontal labels.") - -(eplot-def (x-label-orientation symbol horizontal (horizontal vertical)) - "Orientation of the X labels.") - -(eplot-def (background-image-file string) - "Use an image as the background.") - -(eplot-def (background-image-opacity number 1) - "The opacity of the background image.") - -(eplot-def (background-image-cover symbol all (all plot frame)) - "Position of the background image. -Valid values are `all' (the entire image), `plot' (the plot area) -and `frame' (the surrounding area).") - -(eplot-def (header-file string) - "File where the headers are.") - -(defvar eplot-compact-defaults - '((margin-left 30) - (margin-right 10) - (margin-top 20) - (margin-bottom 21) - (font-size 12) - (x-axis-title-space 3))) - -(defvar eplot-dark-defaults - '((chart-color "#c0c0c0") - (axes-color "#c0c0c0") - (grid-color "#404040") - (background-color "#101010") - (label-color "#c0c0c0") - (legend-color "#c0c0c0") - (legend-background-color "#000000") - (title-color "#c0c0c0"))) - -(defvar eplot-bar-chart-defaults - '((grid-position top) - (grid y) - (grid-opacity 0.2) - (min 0))) - -(defvar eplot-horizontal-bar-chart-defaults - '((grid-position top) - (grid-opacity 0.2) - (min 0))) - -(defclass eplot-chart () - ( - (plots :initarg :plots) - (data :initarg :data) - (xs) - (ys) - (x-values :initform nil) - (x-type :initform nil) - (x-min) - (x-max) - (x-ticks) - (y-ticks) - (y-labels) - (x-labels) - (print-format) - (x-tick-step) - (x-label-step) - (x-step-map :initform nil) - (y-tick-step) - (y-label-step) - (inhibit-compute-x-step :initform nil) - ;; ---- CUT HERE ---- - (axes-color :initarg :axes-color :initform nil) - (background-color :initarg :background-color :initform nil) - (background-gradient :initarg :background-gradient :initform nil) - (background-image-cover :initarg :background-image-cover :initform nil) - (background-image-file :initarg :background-image-file :initform nil) - (background-image-opacity :initarg :background-image-opacity :initform nil) - (bar-font :initarg :bar-font :initform nil) - (bar-font-size :initarg :bar-font-size :initform nil) - (bar-font-weight :initarg :bar-font-weight :initform nil) - (border-color :initarg :border-color :initform nil) - (border-width :initarg :border-width :initform nil) - (chart-color :initarg :chart-color :initform nil) - (font :initarg :font :initform nil) - (font-size :initarg :font-size :initform nil) - (font-weight :initarg :font-weight :initform nil) - (format :initarg :format :initform nil) - (frame-color :initarg :frame-color :initform nil) - (frame-width :initarg :frame-width :initform nil) - (grid :initarg :grid :initform nil) - (grid-color :initarg :grid-color :initform nil) - (grid-opacity :initarg :grid-opacity :initform nil) - (grid-position :initarg :grid-position :initform nil) - (header-file :initarg :header-file :initform nil) - (height :initarg :height :initform nil) - (horizontal-label-font-size :initarg :horizontal-label-font-size :initform nil) - (label-color :initarg :label-color :initform nil) - (label-font :initarg :label-font :initform nil) - (label-font-size :initarg :label-font-size :initform nil) - (layout :initarg :layout :initform nil) - (legend :initarg :legend :initform nil) - (legend-background-color :initarg :legend-background-color :initform nil) - (legend-border-color :initarg :legend-border-color :initform nil) - (legend-color :initarg :legend-color :initform nil) - (margin-bottom :initarg :margin-bottom :initform nil) - (margin-left :initarg :margin-left :initform nil) - (margin-right :initarg :margin-right :initform nil) - (margin-top :initarg :margin-top :initform nil) - (max :initarg :max :initform nil) - (min :initarg :min :initform nil) - (mode :initarg :mode :initform nil) - (surround-color :initarg :surround-color :initform nil) - (title :initarg :title :initform nil) - (title-color :initarg :title-color :initform nil) - (width :initarg :width :initform nil) - (x-axis-title-space :initarg :x-axis-title-space :initform nil) - (x-title :initarg :x-title :initform nil) - (y-title :initarg :y-title :initform nil) - (x-label-format :initarg :x-label-format :initform nil) - (x-label-orientation :initarg :x-label-orientation :initform nil) - (y-label-format :initarg :y-label-format :initform nil) - (horizontal-label-left :initarg :horizontal-label-left :initform nil) - ;; ---- CUT HERE ---- - )) - -;;; Parameters that are plot specific. - -(defmacro eplot-pdef (args doc-string) - (declare (indent defun)) - `(eplot--pdef ',(nth 0 args) ',(nth 1 args) ',(nth 2 args) ',(nth 3 args) - ,doc-string)) - -(defun eplot--pdef (name type default valid doc) - (setq eplot--plot-headers (delq (assq name eplot--plot-headers) - eplot--plot-headers)) - (push (list name - :type type - :default default - :valid valid - :doc doc) - eplot--plot-headers)) - -(eplot-pdef (smoothing symbol nil (moving-average nil)) - "Smoothing algorithm to apply to the data, if any. -Valid values are `moving-average' and, er, probably more to come.") - -(eplot-pdef (gradient string) - "Gradient to apply to the plot. -The syntax is: - - from-color to-color direction position - -The last two parameters are optional. - -direction is either `top-down' (the default), `bottom-up', -`left-right' or `right-left'). - -position is either `below' or `above'. - -to-color can be either a color name, or a string that defines -stops and colors: - - Gradient: black 25-purple-50-white-75-purple-black - -In that case, the second element specifies the percentage points -of where each color ends, so the above starts with black, then at -25% it's purple, then at 50% it's white, then it's back to purple -again at 75%, before ending up at black at a 100% (but you don't -have to include the 100% here -- it's understood).") - -(eplot-pdef (style symbol line ( line impulse point square circle cross - triangle rectangle curve)) - "Style the plot should be drawn in. -Valid values are listed below. Some styles take additional -optional parameters. - -line - Straight lines between values. - -curve - Curved lines between values. - -impulse - size: width of the impulse - -point - -square - -circle - size: diameter of the circle - fill-color: color to fill the center - -cross - size: length of the lines in the cross - -triangle - size: length of the sides of the triangle - fill-color: color to fill the center - -rectangle - size: length of the sides of the rectangle - fill-color: color to fill the center") - -(eplot-pdef (fill-color string) - "Color to use to fill the plot styles that are closed shapes. -I.e., circle, triangle and rectangle.") - -(eplot-pdef (color string (spec chart-color)) - "Color to draw the plot.") - -(eplot-pdef (data-format symbol single (single date time xy)) - "Format of the data. -By default, eplot assumes that each line has a single data point. -This can also be `date', `time' and `xy'. - -date: The first column is a date on ISO8601 format (i.e., YYYYMMDD). - -time: The first column is a clock (i.e., HHMMSS). - -xy: The first column is the X position.") - -(eplot-pdef (data-column number 1) - "Column where the data is.") - -(eplot-pdef (fill-border-color string) - "Border around the fill area when using a fill/gradient style.") - -(eplot-pdef (size number) - "Size of elements in styles that have meaningful sizes.") - -(eplot-pdef (size-factor number) - "Multiply the size of the elements by the value.") - -(eplot-pdef (data-file string) - "File where the data is.") - -(eplot-pdef (data-format symbol-list nil (nil two-values date time)) - "List of symbols to describe the data format. -Elements allowed are `two-values', `date' and `time'.") - -(eplot-pdef (name string) - "Name of the plot, which will be displayed if legends are switched on.") - -(eplot-pdef (legend-color string (spec chart-color)) - "Color for the name to be displayed in the legend.") - -(eplot-pdef (bezier-factor number 0.1) - "The Bezier factor to apply to curve plots.") - -(eplot-pdef (bar-max-width number) - "Max width of bars in bar plots.") - -(defclass eplot-plot () - ( - (values :initarg :values) - ;; ---- CUT HERE ---- - (bar-max-width :initarg :bar-max-width :initform nil) - (bezier-factor :initarg :bezier-factor :initform nil) - (color :initarg :color :initform nil) - (data-column :initarg :data-column :initform nil) - (data-file :initarg :data-file :initform nil) - (data-format :initarg :data-format :initform nil) - (fill-border-color :initarg :fill-border-color :initform nil) - (fill-color :initarg :fill-color :initform nil) - (gradient :initarg :gradient :initform nil) - (legend-color :initarg :legend-color :initform nil) - (name :initarg :name :initform nil) - (size :initarg :size :initform nil) - (size-factor :initarg :size-factor :initform nil) - (smoothing :initarg :smoothing :initform nil) - (style :initarg :style :initform nil) - ;; ---- CUT HERE ---- - )) - -(defun eplot--make-plot (data) - "Make an `eplot-plot' object and initialize based on DATA." - (let ((plot (make-instance 'eplot-plot - :values (cdr (assq :values data))))) - ;; Get the program-defined defaults. - (eplot--object-defaults plot eplot--plot-headers) - ;; One special case. I don't think this hack is quite right... - (when (or (eq (eplot--vs 'mode data) 'dark) - (eq (cdr (assq 'mode eplot--user-defaults)) 'dark)) - (setf (slot-value plot 'color) "#c0c0c0")) - ;; Use the headers. - (eplot--object-values plot (cdr (assq :headers data)) eplot--plot-headers) - plot)) - -(defun eplot--make-chart (data) - "Make an `eplot-chart' object and initialize based on DATA." - (let ((chart (make-instance 'eplot-chart - :plots (mapcar #'eplot--make-plot - (eplot--vs :plots data)) - :data data))) - ;; First get the program-defined defaults. - (eplot--object-defaults chart eplot--chart-headers) - ;; Then do the "meta" variables. - (eplot--meta chart data 'mode 'dark eplot-dark-defaults) - (eplot--meta chart data 'layout 'compact eplot-compact-defaults) - (eplot--meta chart data 'format 'bar-chart eplot-bar-chart-defaults) - (eplot--meta chart data 'format 'horizontal-bar-chart - eplot-horizontal-bar-chart-defaults) - ;; Set defaults from user settings/transients. - (cl-loop for (name . value) in eplot--user-defaults - when (assq name eplot--chart-headers) - do - (setf (slot-value chart name) value) - (eplot--set-dependent-values chart name value)) - ;; Finally, use the data from the chart. - (eplot--object-values chart data eplot--chart-headers) - ;; If not set, recompute the margins based on the font sizes (if - ;; the font size has been changed from defaults). - (when (or (assq 'font-size eplot--user-defaults) - (assq 'font-size data)) - (with-slots ( title x-title y-title - margin-top margin-bottom margin-left - font-size font font-weight) - chart - (when (or title x-title y-title) - (let ((text-height - (eplot--text-height (concat title x-title y-title) - font font-size font-weight))) - (when (and title - (and (not (assq 'margin-top eplot--user-defaults)) - (not (assq 'margin-top data)))) - (cl-incf margin-top (* text-height 1.4))) - (when (and x-title - (and (not (assq 'margin-bottom eplot--user-defaults)) - (not (assq 'margin-bottom data)))) - (cl-incf margin-bottom (* text-height 1.4))) - (when (and y-title - (and (not (assq 'margin-left eplot--user-defaults)) - (not (assq 'margin-left data)))) - (cl-incf margin-left (* text-height 1.4))))))) - chart)) - -(defun eplot--meta (chart data slot value defaults) - (when (or (eq (cdr (assq slot eplot--user-defaults)) value) - (eq (eplot--vy slot data) value)) - (eplot--set-theme chart defaults))) - -(defun eplot--object-defaults (object headers) - (dolist (header headers) - (when-let ((default (plist-get (cdr header) :default))) - (setf (slot-value object (car header)) - ;; Allow overrides via `eplot-set'. - (or (cdr (assq (car header) eplot--user-defaults)) - (if (and (consp default) - (eq (car default) 'spec)) - ;; Chase dependencies. - (eplot--default (cadr default)) - default)))))) - -(defun eplot--object-values (object data headers) - (cl-loop for (name . value) in data - do (unless (eq name :plots) - (let ((spec (cdr (assq name headers)))) - (if (not spec) - (error "%s is not a valid spec" name) - (let ((value - (cl-case (plist-get spec :type) - (number - (string-to-number value)) - (symbol - (intern (downcase value))) - (symbol-list - (mapcar #'intern (split-string (downcase value)))) - (t - value)))) - (setf (slot-value object name) value) - (eplot--set-dependent-values object name value))))))) - -(defun eplot--set-dependent-values (object name value) - (dolist (slot (gethash name (eplot--dependecy-graph))) - (setf (slot-value object slot) value) - (eplot--set-dependent-values object slot value))) - -(defun eplot--set-theme (chart map) - (cl-loop for (slot value) in map - do (setf (slot-value chart slot) value))) - -(defun eplot--default (slot) - "Find the default value for SLOT, chasing dependencies." - (let ((spec (cdr (assq slot eplot--chart-headers)))) - (unless spec - (error "Invalid slot %s" slot)) - (let ((default (plist-get spec :default))) - (if (and (consp default) - (eq (car default) 'spec)) - (eplot--default (cadr default)) - (or (cdr (assq slot eplot--user-defaults)) default))))) - -(defun eplot--dependecy-graph () - (let ((table (make-hash-table))) - (dolist (elem eplot--chart-headers) - (let ((default (plist-get (cdr elem) :default))) - (when (and (consp default) - (eq (car default) 'spec)) - (push (car elem) (gethash (cadr default) table))))) - table)) - -(defun eplot--render (data &optional return-image) - "Create the chart and display it. -If RETURN-IMAGE is non-nil, return it instead of displaying it." - (let* ((chart (eplot--make-chart data)) - svg) - (with-slots ( width height xs ys - margin-left margin-right margin-top margin-bottom - grid-position plots x-min format - x-label-orientation) - chart - ;; Set the size of the chart based on the window it's going to - ;; be displayed in. It uses the *eplot* window by default, or - ;; the current one if that isn't displayed. - (let ((factor (image-compute-scaling-factor image-scaling-factor))) - (unless width - (setq width (truncate - (/ (* (window-pixel-width - (get-buffer-window "*eplot*" t)) - 0.9) - factor)))) - (unless height - (setq height (truncate - (/ (* (window-pixel-height - (get-buffer-window "*eplot*" t)) - 0.9) - factor))))) - (setq svg (svg-create width height) - xs (- width margin-left margin-right) - ys (- height margin-top margin-bottom)) - ;; Protect against being called in an empty buffer. - (if (not (and plots - ;; Sanity check against the user choosing dimensions - ;; that leave no space for the plot. - (> ys 0) (> xs 0))) - ;; Just draw the basics. - (eplot--draw-basics svg chart) - - ;; Horizontal bar charts are special. - (when (eq format 'horizontal-bar-chart) - (eplot--adjust-horizontal-bar-chart chart data)) - ;; Compute min/max based on all plots, and also compute x-ticks - ;; etc. - (eplot--compute-chart-dimensions chart) - (when (and (eq x-label-orientation 'vertical) - (eplot--default-p 'margin-bottom (slot-value chart 'data))) - (eplot--adjust-vertical-x-labels chart)) - ;; Analyze values and adjust values accordingly. - (eplot--adjust-chart chart) - ;; Compute the Y labels -- this may adjust `margin-left'. - (eplot--compute-y-labels chart) - ;; Compute the X labels -- this may adjust `margin-bottom'. - (eplot--compute-x-labels chart) - ;; Draw background/borders/titles/etc. - (eplot--draw-basics svg chart) - - (when (eq grid-position 'top) - (eplot--draw-plots svg chart)) - - (eplot--draw-x-ticks svg chart) - (unless (eq format 'horizontal-bar-chart) - (eplot--draw-y-ticks svg chart)) - - ;; Draw axes. - (with-slots ( margin-left margin-right margin-margin-top - margin-bottom axes-color) - chart - (svg-line svg margin-left margin-top margin-left - (+ (- height margin-bottom) 5) - :stroke axes-color) - (svg-line svg (- margin-left 5) (- height margin-bottom) - (- width margin-right) (- height margin-bottom) - :stroke axes-color)) - - (when (eq grid-position 'bottom) - (eplot--draw-plots svg chart))) - - (with-slots (frame-color frame-width) chart - (when (or frame-color frame-width) - (svg-rectangle svg margin-left margin-top xs ys - :stroke-width frame-width - :fill "none" - :stroke-color frame-color))) - (eplot--draw-legend svg chart)) - - (if return-image - svg - (svg-insert-image svg) - chart))) - -(defun eplot--adjust-horizontal-bar-chart (chart data) - (with-slots ( plots bar-font bar-font-size bar-font-weight margin-left - width margin-right xs) - chart - (dolist (plot plots) - (with-slots ( data-format values) plot - (push 'xy data-format) - ;; Flip the values -- we want the values to be on the X - ;; axis instead. - (setf values - (cl-loop for value in values - for i from 1 - collect (list :value i - :x (plist-get value :value) - :settings - (plist-get value :settings)))) - (when (eplot--default-p 'margin-left data) - (setf margin-left - (+ (cl-loop for value in values - maximize - (eplot--text-width - (eplot--vs 'label (plist-get value :settings)) - bar-font bar-font-size bar-font-weight)) - 20) - xs (- width margin-left margin-right))))))) - -(defun eplot--draw-basics (svg chart) - (with-slots ( width height - chart-color font font-size font-weight - margin-left margin-right margin-top margin-bottom - background-color label-color - xs ys) - chart - ;; Add background. - (eplot--draw-background chart svg 0 0 width height) - (with-slots ( background-image-file background-image-opacity - background-image-cover) - chart - (when (and background-image-file - ;; Sanity checks to avoid erroring out later. - (file-exists-p background-image-file) - (file-regular-p background-image-file)) - (apply #'svg-embed svg background-image-file "image/jpeg" nil - :opacity background-image-opacity - :preserveAspectRatio "xMidYMid slice" - (if (memq background-image-cover '(all frame)) - `(:x 0 :y 0 :width ,width :height ,height) - `(:x ,margin-left :y ,margin-top :width ,xs :height ,ys))) - (when (eq background-image-cover 'frame) - (eplot--draw-background chart svg margin-left margin-right xs ys)))) - ;; Area between plot and edges. - (with-slots (surround-color) chart - (when surround-color - (svg-rectangle svg 0 0 width height - :fill surround-color) - (svg-rectangle svg margin-left margin-top - xs ys - :fill background-color))) - ;; Border around the entire chart. - (with-slots (border-width border-color) chart - (when (or border-width border-color) - (svg-rectangle svg 0 0 width height - :stroke-width (or border-width 1) - :fill "none" - :stroke-color (or border-color chart-color)))) - ;; Frame around the plot. - (with-slots (frame-width frame-color) chart - (when (or frame-width frame-color) - (svg-rectangle svg margin-left margin-top xs ys - :stroke-width (or frame-width 1) - :fill "none" - :stroke-color (or frame-color chart-color)))) - ;; Title and legends. - (with-slots (title title-color) chart - (when title - (svg-text svg title - :font-family font - :text-anchor "middle" - :font-weight font-weight - :font-size font-size - :fill title-color - :x (+ margin-left (/ (- width margin-left margin-right) 2)) - :y (+ 3 (/ margin-top 2))))) - (with-slots (x-title) chart - (when x-title - (svg-text svg x-title - :font-family font - :text-anchor "middle" - :font-weight font-weight - :font-size font-size - :fill label-color - :x (+ margin-left (/ (- width margin-left margin-right) 2)) - :y (- height (/ margin-bottom 4))))) - (with-slots (y-title) chart - (when y-title - (let ((text-height - (eplot--text-height y-title font font-size font-weight))) - (svg-text svg y-title - :font-family font - :text-anchor "middle" - :font-weight font-weight - :font-size font-size - :fill label-color - :transform - (format "translate(%s,%s) rotate(-90)" - (- (/ margin-left 2) (/ text-height 2) 4) - (+ margin-top - (/ (- height margin-bottom margin-top) 2))))))))) - -(defun eplot--draw-background (chart svg left top width height) - (with-slots (background-gradient background-color) chart - (let ((gradient (eplot--parse-gradient background-gradient)) - id) - (when gradient - (setq id (format "gradient-%s" (make-temp-name "grad"))) - (eplot--gradient svg id 'linear - (eplot--stops (eplot--vs 'from gradient) - (eplot--vs 'to gradient)) - (eplot--vs 'direction gradient))) - (apply #'svg-rectangle svg left top width height - (if gradient - `(:gradient ,id) - `(:fill ,background-color)))))) - -(defun eplot--compute-chart-dimensions (chart) - (with-slots ( min max plots x-values x-min x-max x-ticks - print-format font-size - xs - inhibit-compute-x-step x-type x-step-map format - x-tick-step x-label-step - label-font label-font-size x-label-format) - chart - (let ((set-min min) - (set-max max)) - (dolist (plot plots) - (with-slots (values data-format) plot - (let* ((vals (nconc (seq-map (lambda (v) (plist-get v :value)) values) - (and (memq 'two-values data-format) - (seq-map - (lambda (v) (plist-get v :extra-value)) - values))))) - ;; Set the x-values based on the first plot. - (unless x-values - (setq print-format (cond - ((memq 'year data-format) 'year) - ((memq 'date data-format) 'date) - ((memq 'time data-format) 'time) - (t 'number))) - (cond - ((or (memq 'xy data-format) - (memq 'year data-format)) - (setq x-values (cl-loop for val in values - collect (plist-get val :x)) - x-min (if (eq format 'horizontal-bar-chart) - 0 - (seq-min x-values)) - x-max (seq-max x-values) - x-ticks (eplot--get-ticks x-min x-max xs)) - (when (memq 'year data-format) - (setq print-format 'literal-year))) - ((memq 'date data-format) - (setq x-values - (cl-loop for val in values - collect - (time-to-days - (encode-time - (decoded-time-set-defaults - (iso8601-parse-date - (format "%d" (plist-get val :x))))))) - x-min (seq-min x-values) - x-max (seq-max x-values) - inhibit-compute-x-step t) - (let ((xs (eplot--get-date-ticks - x-min x-max xs - label-font label-font-size x-label-format))) - (setq x-ticks (car xs) - print-format (cadr xs) - x-tick-step 1 - x-label-step 1 - x-step-map (nth 2 xs)))) - ((memq 'time data-format) - (setq x-values - (cl-loop for val in values - collect - (time-convert - (encode-time - (decoded-time-set-defaults - (iso8601-parse-time - (format "%06d" (plist-get val :x))))) - 'integer)) - x-min (car x-values) - x-max (car (last x-values)) - inhibit-compute-x-step t) - (let ((xs (eplot--get-time-ticks - x-min x-max xs label-font label-font-size - x-label-format))) - (setq x-ticks (car xs) - print-format (cadr xs) - x-tick-step 1 - x-label-step 1 - x-step-map (nth 2 xs)))) - (t - ;; This is a one-dimensional plot -- we don't have X - ;; values, really, so we just do zero to (1- (length - ;; values)). - (setq x-type 'one-dimensional - x-values (cl-loop for i from 0 - repeat (length values) - collect i) - x-min (car x-values) - x-max (car (last x-values)) - x-ticks x-values)))) - (unless set-min - (setq min (min (or min 1.0e+INF) (seq-min vals)))) - (unless set-max - (setq max (max (or max -1.0e+INF) (seq-max vals)))))))))) - -(defun eplot--adjust-chart (chart) - (with-slots ( x-tick-step x-label-step y-tick-step y-label-step - min max ys format inhibit-compute-x-step - y-ticks xs x-values print-format - x-label-format label-font label-font-size data - x-ticks) - chart - (setq y-ticks (and max - (eplot--get-ticks - min - ;; We get 5% more ticks to check whether we - ;; should extend max. - (if (eplot--default-p 'max data) - (* max 1.05) - max) - ys))) - (when (eplot--default-p 'max data) - (setq max (max max (car (last y-ticks))))) - (if (eq format 'bar-chart) - (setq x-tick-step 1 - x-label-step 1) - (unless inhibit-compute-x-step - (let ((xt (eplot--compute-x-ticks - xs x-ticks print-format - x-label-format label-font label-font-size))) - (setq x-tick-step (car xt) - x-label-step (cadr xt))))) - (when max - (let ((yt (eplot--compute-y-ticks - ys y-ticks - (eplot--text-height "100" label-font label-font-size)))) - (setq y-tick-step (car yt) - y-label-step (cadr yt)))) - ;; If max is less than 2% off from a pleasant number, then - ;; increase max. - (when (eplot--default-p 'max data) - (cl-loop for tick in (reverse y-ticks) - when (and (< max tick) - (< (e/ (- tick max) (- max min)) 0.02)) - return (progn - (setq max tick) - ;; Chop off any further ticks. - (setcdr (member tick y-ticks) nil)))) - - (when y-ticks - (if (and (eplot--default-p 'min data) - (< (car y-ticks) min)) - (setq min (car y-ticks)) - ;; We may be extending the bottom of the chart to get pleasing - ;; numbers. We don't want to be drawing the chart on top of the - ;; X axis, because the chart won't be visible there. - (when (and nil - (<= min (car y-ticks)) - ;; But not if we start at origo, because that just - ;; looks confusing. - (not (zerop min))) - (setq min (- (car y-ticks) - ;; 2% of the value range. - (* 0.02 (- (car (last y-ticks)) (car y-ticks)))))))))) - -(defun eplot--adjust-vertical-x-labels (chart) - (with-slots ( x-step-map x-ticks format plots - print-format x-label-format label-font - label-font-size margin-bottom - bar-font bar-font-size bar-font-weight) - chart - ;; Make X ticks. - (let ((width - (cl-loop - for xv in (or x-step-map x-ticks) - for x = (if (consp xv) (car xv) xv) - for i from 0 - for value = (and (equal format 'bar-chart) - (elt (slot-value (car plots) 'values) i)) - for label = (if (equal format 'bar-chart) - (eplot--vs 'label - (plist-get value :settings) - ;; When we're doing bar charts, we - ;; want default labeling to start with - ;; 1 and not zero. - (format "%s" (1+ x))) - (eplot--format-value x print-format x-label-format)) - maximize (if (equal format 'bar-chart) - (eplot--text-width - label bar-font bar-font-size bar-font-weight) - (eplot--text-width - label label-font label-font-size))))) - ;; Ensure that we have enough room to display the X labels - ;; (unless overridden). - (with-slots ( height margin-top ys - y-ticks y-tick-step y-label-step min max) - chart - (setq margin-bottom (max margin-bottom (+ width 40)) - ys (- height margin-top margin-bottom)))))) - -(defun eplot--compute-x-labels (chart) - (with-slots ( x-step-map x-ticks - format plots print-format x-label-format x-labels - x-tick-step x-label-step - x-label-orientation margin-bottom) - chart - ;; Make X ticks. - (setf x-labels - (cl-loop - for xv in (or x-step-map x-ticks) - for x = (if (consp xv) (car xv) xv) - for do-tick = (if (consp xv) - (nth 1 xv) - (zerop (e% x x-tick-step))) - for do-label = (if (consp xv) - (nth 2 xv) - (zerop (e% x x-label-step))) - for i from 0 - for value = (and (equal format 'bar-chart) - (elt (slot-value (car plots) 'values) i)) - collect (list - (if (equal format 'bar-chart) - (eplot--vs 'label - (plist-get value :settings) - ;; When we're doing bar charts, we - ;; want default labeling to start with - ;; 1 and not zero. - (format "%s" (1+ x))) - (eplot--format-value x print-format x-label-format)) - do-tick - do-label))))) - -(defun eplot--draw-x-ticks (svg chart) - (with-slots ( x-step-map x-ticks format layout print-format - margin-left margin-right margin-top margin-bottom - x-min x-max xs - width height - axes-color label-color - grid grid-opacity grid-color - font x-tick-step x-label-step x-label-format x-label-orientation - label-font label-font-size - plots x-labels - x-values - bar-font bar-font-size bar-font-weight - plots) - chart - (let ((font label-font) - (font-size label-font-size) - (font-weight 'normal) - (label-settings nil)) - (when (equal format 'bar-chart) - (setq font bar-font - font-size bar-font-size - font-weight bar-font-weight - label-settings (mapcar (lambda (e) - (plist-get e :settings)) - (slot-value (car plots) 'values)))) - ;; Make X ticks. - (cl-loop with label-height - for xv in (or x-step-map x-ticks) - for x = (if (consp xv) (car xv) xv) - for i from 0 - for (label do-tick do-label) in x-labels - for px = (if (equal format 'bar-chart) - (+ margin-left - (/ (/ xs (length x-values)) 2) - (* (e/ i (length x-values)) - xs)) - (+ margin-left - (* (/ (- (* 1.0 x) x-min) (- x-max x-min)) - xs))) - for this-font-weight = - (if (equal format 'bar-chart) - (or (cdr (assq 'label-font-weight (nth i label-settings))) - font-weight) - font-weight) - ;; We might have one extra stride outside the area -- don't - ;; draw it. - when (<= px (- width margin-right)) - do - (when do-tick - ;; Draw little tick. - (unless (equal format 'bar-chart) - (svg-line svg - px (- height margin-bottom) - px (+ (- height margin-bottom) - (if do-label - 4 - 2)) - :stroke axes-color)) - (when (or (eq grid 'xy) (eq grid 'x)) - (svg-line svg px margin-top - px (- height margin-bottom) - :opacity grid-opacity - :stroke grid-color))) - (when (and do-label - ;; We want to skip marking the first X value - ;; unless we're a bar chart or we're a one - ;; dimensional chart. - (or (equal format 'bar-chart) - t - (not (= x-min (car x-values))) - (eq x-type 'one-dimensional) - (and (not (zerop x)) (not (zerop i))))) - (if (eq x-label-orientation 'vertical) - (progn - (unless label-height - ;; The X position we're putting the label at is - ;; based on the bottom of the lower-case - ;; characters. So we want to ignore descenders - ;; etc, so we use "xx" to determine the height - ;; to be able to center the text. - (setq label-height - (eplot--text-height - ;; If the labels are numerical, we need - ;; to center them using the height of - ;; numbers. - (if (string-match "^[0-9]+$" label) - "10" - ;; Otherwise center them on the baseline. - "xx") - font font-size this-font-weight))) - (svg-text svg label - :font-family font - :text-anchor "end" - :font-size font-size - :font-weight this-font-weight - :fill label-color - :transform - (format "translate(%s,%s) rotate(-90)" - (+ px (/ label-height 2)) - (- height margin-bottom -10)))) - (svg-text svg label - :font-family font - :text-anchor "middle" - :font-size font-size - :font-weight this-font-weight - :fill label-color - :x px - :y (+ (- height margin-bottom) - font-size - (if (equal format 'bar-chart) - (if (equal layout 'compact) 3 5) - 2))))))))) - -(defun eplot--stride (chart values) - (with-slots (xs x-type format) chart - (if (eq x-type 'one-dimensional) - (e/ xs - ;; Fenceposting bar-chart vs everything else. - (if (eq format 'bar-chart) - (length values) - (1- (length values)))) - (e/ xs (length values))))) - -(defun eplot--default-p (slot data) - "Return non-nil if SLOT is at the default value." - (and (not (assq slot eplot--user-defaults)) - (not (assq slot data)))) - -(defun eplot--compute-y-labels (chart) - (with-slots ( y-ticks y-labels - width height min max xs ys - margin-top margin-bottom margin-left margin-right - y-tick-step y-label-step y-label-format) - chart - ;; First collect all the labels we're thinking about outputting. - (setq y-labels - (cl-loop for y in y-ticks - for py = (- (- height margin-bottom) - (* (/ (- (* 1.0 y) min) (- max min)) - ys)) - when (and (<= margin-top py (- height margin-bottom)) - (zerop (e% y y-tick-step)) - (zerop (e% y y-label-step))) - collect (eplot--format-y - y (- (cadr y-ticks) (car y-ticks)) nil - y-label-format))) - ;; Check the labels to see whether we have too many digits for - ;; what we're actually going to display. Man, this is a lot of - ;; back-and-forth and should be rewritten to be less insanely - ;; inefficient. - (when (= (seq-count (lambda (label) - (string-match "\\." label)) - y-labels) - (length y-labels)) - (setq y-labels - (cl-loop with max = (cl-loop for label in y-labels - maximize (eplot--decimal-digits - (string-to-number label))) - for label in y-labels - collect (format (if (zerop max) - "%d" - (format "%%.%df" max)) - (string-to-number label))))) - (setq y-labels (cl-coerce y-labels 'vector)) - ;; Ensure that we have enough room to display the Y labels - ;; (unless overridden). - (when (eplot--default-p 'margin-left (slot-value chart 'data)) - (with-slots (label-font label-font-size) chart - (setq margin-left (max margin-left - (+ (eplot--text-width - (elt y-labels (1- (length y-labels))) - label-font label-font-size) - 10)) - xs (- width margin-left margin-right)))))) - -(defun eplot--draw-y-ticks (svg chart) - (with-slots ( y-ticks y-labels y-tick-step y-label-step label-color - label-font label-font-size - width height min max ys - margin-top margin-bottom margin-left margin-right - axes-color - grid grid-opacity grid-color) - chart - ;; Make Y ticks. - (cl-loop with lnum = 0 - with text-height = (eplot--text-height - "012" label-font label-font-size) - for y in y-ticks - for i from 0 - for py = (- (- height margin-bottom) - (* (/ (- (* 1.0 y) min) (- max min)) - ys)) - do - (when (and (<= margin-top py (- height margin-bottom)) - (zerop (e% y y-tick-step))) - (svg-line svg margin-left py - (- margin-left 3) py - :stroke-color axes-color) - (when (or (eq grid 'xy) (eq grid 'y)) - (svg-line svg margin-left py - (- width margin-right) py - :opacity grid-opacity - :stroke-color grid-color)) - (when (zerop (e% y y-label-step)) - (svg-text svg (elt y-labels lnum) - :font-family label-font - :text-anchor "end" - :font-size label-font-size - :fill label-color - :x (- margin-left 6) - :y (+ py (/ text-height 2) -1)) - (cl-incf lnum)))))) - -(defun eplot--text-width (text font font-size &optional font-weight) - (string-pixel-width - (propertize text 'face - (list :font (font-spec :family font - :weight (or font-weight 'normal) - :size font-size))))) - -(defvar eplot--text-size-cache (make-hash-table :test #'equal)) - -(defun eplot--text-height (text font font-size &optional font-weight) - (cdr (eplot--text-size text font font-size font-weight))) - -(defun eplot--text-size (text font font-size font-weight) - (let ((key (list text font font-size font-weight))) - (or (gethash key eplot--text-size-cache) - (let ((size (eplot--text-size-1 text font font-size font-weight))) - (setf (gethash key eplot--text-size-cache) size) - size)))) - -(defun eplot--text-size-1 (text font font-size font-weight) - (if (not (executable-find "convert")) - ;; This "default" text size is kinda bogus. - (cons (* (length text) font-size) font-size) - (let* ((size (* font-size 10)) - (svg (svg-create size size)) - text-size) - (svg-rectangle svg 0 0 size size :fill "black") - (svg-text svg text - :font-family font - :text-anchor "middle" - :font-size font-size - :font-weight (or font-weight 'normal) - :fill "white" - :x (/ size 2) - :y (/ size 2)) - (with-temp-buffer - (set-buffer-multibyte nil) - (svg-print svg) - (let* ((file (make-temp-file "eplot" nil ".svg")) - (png (file-name-with-extension file ".png"))) - (unwind-protect - (progn - (write-region (point-min) (point-max) file nil 'silent) - ;; rsvg-convert is 5x faster than convert when doing SVG, so - ;; if we have it, we use it. - (when (executable-find "rsvg-convert") - (unwind-protect - (call-process "rsvg-convert" nil nil nil - (format "--output=%s" png) file) - (when (file-exists-p png) - (delete-file file) - (setq file png)))) - (erase-buffer) - (when (zerop (call-process "convert" nil t nil - "-trim" "+repage" file "info:-")) - (goto-char (point-min)) - (when (re-search-forward " \\([0-9]+\\)x\\([0-9]+\\)" nil t) - (setq text-size - (cons (string-to-number (match-string 1)) - (string-to-number (match-string 2))))))) - (when (file-exists-p file) - (delete-file file))))) - (or text-size - ;; This "default" text size is kinda bogus. - (cons (* (length text) font-size) font-size))))) - -(defun eplot--draw-legend (svg chart) - (with-slots ( legend plots - margin-left margin-top - font font-size font-weight - background-color axes-color - legend-color legend-background-color legend-border-color) - chart - (when (eq legend 'true) - (when-let ((names - (cl-loop for plot in plots - for name = (slot-value plot 'name) - when name - collect - (cons name (slot-value plot 'color))))) - (svg-rectangle svg (+ margin-left 20) (+ margin-top 20) - (format "%dpx" - (+ 10 - (seq-max - (mapcar (lambda (name) - (eplot--text-width (car name) - font font-size)) - names)))) - (* font-size (+ (length names) 2)) - :font-size font-size - :fill-color legend-background-color - :stroke-color legend-border-color) - (cl-loop for name in names - for i from 0 - do (svg-text svg (car name) - :font-family font - :text-anchor "front" - :font-size font-size - :font-weight font-weight - :fill (or (cdr name) legend-color) - :x (+ margin-left 25) - :y (+ margin-top 40 (* i font-size)))))))) - -(defun eplot--format-y (y spacing whole format-string) - (format (or format-string "%s") - (cond - ((or (= (round (* spacing 100)) 10) (= (round (* spacing 100)) 20)) - (format "%.1f" y)) - ((< spacing 0.01) - (format "%.3f" y)) - ((< spacing 1) - (format "%.2f" y)) - ((and (< spacing 1) (not (zerop (mod (* spacing 10) 1)))) - (format "%.1f" y)) - ((zerop (% spacing 1000000000)) - (format "%dG" (/ y 1000000000))) - ((zerop (% spacing 1000000)) - (format "%dM" (/ y 1000000))) - ((zerop (% spacing 1000)) - (format "%dk" (/ y 1000))) - ((>= spacing 1) - (format "%s" y)) - ((not whole) - (format "%.1f" y)) - (t - (format "%s" y))))) - -(defun eplot--format-value (value print-format label-format) - (replace-regexp-in-string - ;; Texts in SVG collapse multiple spaces into one. So do it here, - ;; too, so that width calculations are correct. - " +" " " - (cond - ((eq print-format 'date) - (format-time-string - (or label-format "%Y-%m-%d") (eplot--days-to-time value))) - ((eq print-format 'year) - (format-time-string (or label-format "%Y") (eplot--days-to-time value))) - ((eq print-format 'time) - (format-time-string (or label-format "%H:%M:%S") value)) - ((eq print-format 'minute) - (format-time-string (or label-format "%H:%M") value)) - ((eq print-format 'hour) - (format-time-string (or label-format "%H") value)) - (t - (format (or label-format "%s") value))))) - -(defun eplot--compute-x-ticks (xs x-values print-format x-label-format - label-font label-font-size) - (let* ((min (seq-min x-values)) - (max (seq-max x-values)) - (count (length x-values)) - (max-print (eplot--format-value max print-format x-label-format)) - ;; We want each label to be spaced at least as long apart as - ;; the length of the longest label, with room for two blanks - ;; in between. - (min-spacing (* 1.2 (eplot--text-width max-print label-font - label-font-size))) - (digits (eplot--decimal-digits (- (cadr x-values) (car x-values)))) - (every (e/ 1 (expt 10 digits)))) - (cond - ;; We have room for every X value. - ((< (* count min-spacing) xs) - (list every every)) - ;; We have to prune X labels, but not grid lines. (We shouldn't - ;; have a grid line more than every 10 pixels.) - ((< (* count 10) xs) - (list every - (let ((label-step every)) - (while (> (/ (- max min) label-step) (/ xs min-spacing)) - (setq label-step (eplot--next-weed label-step))) - label-step))) - ;; We have to reduce both grid lines and labels. - (t - (let ((tick-step every)) - (while (> (/ (- max min) tick-step) (/ xs 10)) - (setq tick-step (eplot--next-weed tick-step))) - (list tick-step - (let ((label-step tick-step)) - (while (> (/ (- max min) label-step) (/ xs min-spacing)) - (setq label-step (eplot--next-weed label-step)) - (while (not (zerop (% label-step tick-step))) - (setq label-step (eplot--next-weed label-step)))) - label-step))))))) - -(defun eplot--compute-y-ticks (ys y-values text-height) - (let* ((min (car y-values)) - (max (car (last y-values))) - (count (length y-values)) - ;; We want each label to be spaced at least as long apart as - ;; the height of the label. - (min-spacing (+ text-height 10)) - (digits (eplot--decimal-digits (- (cadr y-values) (car y-values)))) - (every (e/ 1 (expt 10 digits)))) - (cond - ;; We have room for every X value. - ((< (* count min-spacing) ys) - (list every every)) - ;; We have to prune Y labels, but not grid lines. (We shouldn't - ;; have a grid line more than every 10 pixels.) - ((< (* count 10) ys) - (list every - (let ((label-step every)) - (while (> (/ (- max min) label-step) (/ ys min-spacing)) - (setq label-step (eplot--next-weed label-step))) - label-step))) - ;; We have to reduce both grid lines and labels. - (t - (let ((tick-step 1)) - (while (> (/ count tick-step) (/ ys 10)) - (setq tick-step (eplot--next-weed tick-step))) - (list tick-step - (let ((label-step tick-step)) - (while (> (/ (- max min) label-step) (/ ys min-spacing)) - (setq label-step (eplot--next-weed label-step))) - label-step))))))) - -(defvar eplot--pleasing-numbers '(1 2 5 10)) - -(defun eplot--next-weed (weed) - (let (digits series) - (if (>= weed 1) - (setq digits (truncate (log weed 10)) - series (/ weed (expt 10 digits))) - (setq digits (eplot--decimal-digits weed) - series (truncate (* weed (expt 10 digits))))) - (let ((next (cadr (memq series eplot--pleasing-numbers)))) - (unless next - (error "Invalid weed: %s" weed)) - (if (>= weed 1) - (* next (expt 10 digits)) - (e/ next (expt 10 digits)))))) - -(defun eplot--parse-gradient (string) - (when string - (let ((bits (split-string string))) - (list - (cons 'from (nth 0 bits)) - (cons 'to (nth 1 bits)) - (cons 'direction (intern (or (nth 2 bits) "top-down"))) - (cons 'position (intern (or (nth 3 bits) "below"))))))) - -(defun eplot--smooth (values algo xs) - (if (not algo) - values - (let* ((vals (cl-coerce values 'vector)) - (max (1- (length vals))) - (period (* 4 (ceiling (/ max xs))))) - (cl-case algo - (moving-average - (cl-loop for i from 0 upto max - collect (e/ (cl-loop for ii from 0 upto (1- period) - sum (elt vals (min (+ i ii) max))) - period))))))) - -(defun eplot--vary-color (color n) - (let ((colors ["#e6194b" "#3cb44b" "#ffe119" "#4363d8" "#f58231" "#911eb4" - "#46f0f0" "#f032e6" "#bcf60c" "#fabebe" "#008080" "#e6beff" - "#9a6324" "#fffac8" "#800000" "#aaffc3" "#808000" "#ffd8b1" - "#000075" "#808080" "#ffffff" "#000000"])) - (unless (equal color "vary") - (setq colors - (if (string-search " " color) - (split-string color) - (list color)))) - (elt colors (mod n (length colors))))) - -(defun eplot--pv (plot slot &optional default) - (let ((user (cdr (assq slot eplot--user-defaults)))) - (when (and (stringp user) (zerop (length user))) - (setq user nil)) - (or user (slot-value plot slot) default))) - -(defun eplot--draw-plots (svg chart) - (if (eq (slot-value chart 'format) 'horizontal-bar-chart) - (eplot--draw-horizontal-bar-chart svg chart) - (eplot--draw-normal-plots svg chart))) - -(defun eplot--draw-normal-plots (svg chart) - (with-slots ( plots chart-color height format - margin-bottom margin-left - min max xs ys - margin-top - x-values x-min x-max - label-font label-font-size) - chart - ;; Draw all the plots. - (cl-loop for plot in (reverse plots) - for plot-number from 0 - for values = (slot-value plot 'values) - for vals = (eplot--smooth - (seq-map (lambda (v) (plist-get v :value)) values) - (slot-value plot 'smoothing) - xs) - for polygon = nil - for gradient = (eplot--parse-gradient (eplot--pv plot 'gradient)) - for lpy = nil - for lpx = nil - for style = (if (eq format 'bar-chart) - 'bar - (slot-value plot 'style)) - for bar-max-width = (eplot--pv plot 'bar-max-width) - for bar-width = (and (eq style 'bar) - (min (or bar-max-width most-positive-fixnum) - (/ xs (length x-values)))) - for bar-gap = (and (eq style 'bar) - (if (< bar-width - (or bar-max-width most-positive-fixnum)) - (* bar-width 0.1) - 0)) - for clip-id = (format "url(#clip-%d)" plot-number) - do - (svg--append - svg - (dom-node 'clipPath - `((id . ,(format "clip-%d" plot-number))) - (dom-node 'rect - `((x . ,margin-left) - (y . , margin-top) - (width . ,xs) - (height . ,ys))))) - (unless gradient - (when-let ((fill (slot-value plot 'fill-color))) - (setq gradient `((from . ,fill) (to . ,fill) - (direction . top-down) (position . below))))) - (when gradient - (if (eq (eplot--vs 'position gradient) 'above) - (push (cons margin-left margin-top) polygon) - (push (cons margin-left (- height margin-bottom)) polygon))) - (cl-loop - for val in vals - for value in values - for x in x-values - for i from 0 - for settings = (plist-get value :settings) - for color = (eplot--vary-color - (eplot--vs 'color settings (slot-value plot 'color)) - i) - for py = (- (- height margin-bottom) - (* (/ (- (* 1.0 val) min) (- max min)) - ys)) - for px = (if (eq style 'bar) - (+ margin-left - (/ (/ xs (length x-values)) 2) - (* (e/ i (length x-values)) - xs)) - (+ margin-left - (* (e/ (- x x-min) (- x-max x-min)) - xs))) - do - ;; Some data points may have texts. - (when-let ((text (eplot--vs 'text settings))) - (svg-text svg text - :font-family label-font - :text-anchor "middle" - :font-size label-font-size - :font-weight 'normal - :fill color - :x px - :y (- py (eplot--text-height - text label-font label-font-size) - -5))) - ;; You may mark certain points. - (when-let ((mark (eplot--vy 'mark settings))) - (cl-case mark - (cross - (let ((s (eplot--element-size val plot settings 3))) - (svg-line svg (- px s) (- py s) - (+ px s) (+ py s) - :clip-path clip-id - :stroke color) - (svg-line svg (+ px s) (- py s) - (- px s) (+ py s) - :clip-path clip-id - :stroke color))) - (otherwise - (svg-circle svg px py 3 - :fill color)))) - (cl-case style - (bar - (if (not gradient) - (svg-rectangle - svg - (+ (- px (e/ bar-width 2)) (e/ bar-gap 2)) - py - (- bar-width bar-gap) - (- height margin-bottom py) - :clip-path clip-id - :fill color) - (let ((id (format "gradient-%s" (make-temp-name "grad")))) - (eplot--gradient svg id 'linear - (eplot--stops (eplot--vs 'from gradient) - (eplot--vs 'to gradient)) - (eplot--vs 'direction gradient)) - (svg-rectangle - svg - (+ (- px (e/ bar-width 2)) (e/ bar-gap 2)) - py - (- bar-width bar-gap) - (- height margin-bottom py) - :clip-path clip-id - :gradient id)))) - (impulse - (let ((width (eplot--element-size val plot settings 1))) - (if (= width 1) - (svg-line svg - px py - px (- height margin-bottom) - :clip-path clip-id - :stroke color) - (svg-rectangle svg - (- px (e/ width 2)) py - width (- height py margin-bottom) - :clip-path clip-id - :fill color)))) - (point - (svg-line svg px py (1+ px) (1+ py) - :clip-path clip-id - :stroke color)) - (line - ;; If we're doing a gradient, we're just collecting - ;; points and will draw the polygon later. - (if gradient - (push (cons px py) polygon) - (when lpx - (svg-line svg lpx lpy px py - :stroke-width (eplot--pv plot 'size 1) - :clip-path clip-id - :stroke color)))) - (curve - (push (cons px py) polygon)) - (square - (if gradient - (progn - (when lpx - (push (cons lpx py) polygon)) - (push (cons px py) polygon)) - (when lpx - (svg-line svg lpx lpy px lpy - :clip-path clip-id - :stroke color) - (svg-line svg px lpy px py - :clip-path clip-id - :stroke color)))) - (circle - (svg-circle svg px py - (eplot--element-size val plot settings 3) - :clip-path clip-id - :stroke color - :fill (eplot--vary-color - (eplot--vs - 'fill-color settings - (or (slot-value plot 'fill-color) "none")) - i))) - (cross - (let ((s (eplot--element-size val plot settings 3))) - (svg-line svg (- px s) (- py s) - (+ px s) (+ py s) - :clip-path clip-id - :stroke color) - (svg-line svg (+ px s) (- py s) - (- px s) (+ py s) - :clip-path clip-id - :stroke color))) - (triangle - (let ((s (eplot--element-size val plot settings 5))) - (svg-polygon svg - (list - (cons (- px (e/ s 2)) (+ py (e/ s 2))) - (cons px (- py (e/ s 2))) - (cons (+ px (e/ s 2)) (+ py (e/ s 2)))) - :clip-path clip-id - :stroke color - :fill-color - (or (slot-value plot 'fill-color) "none")))) - (rectangle - (let ((s (eplot--element-size val plot settings 3))) - (svg-rectangle svg (- px (e/ s 2)) (- py (e/ s 2)) - s s - :clip-path clip-id - :stroke color - :fill-color - (or (slot-value plot 'fill-color) "none"))))) - (setq lpy py - lpx px)) - - ;; We're doing a gradient of some kind (or a curve), so - ;; draw it now when we've collected the polygon. - (when polygon - ;; We have a "between" chart, so collect the data points - ;; from the "extra" values, too. - (when (memq 'two-values (slot-value plot 'data-format)) - (cl-loop - for val in (nreverse - (seq-map (lambda (v) (plist-get v :extra-value)) - values)) - for x from (1- (length vals)) downto 0 - for py = (- (- height margin-bottom) - (* (/ (- (* 1.0 val) min) (- max min)) - ys)) - for px = (+ margin-left - (* (e/ (- x x-min) (- x-max x-min)) - xs)) - do - (cl-case style - (line - (push (cons px py) polygon)) - (square - (when lpx - (push (cons lpx py) polygon)) - (push (cons px py) polygon))) - (setq lpx px lpy py))) - (when gradient - (if (eq (eplot--vs 'position gradient) 'above) - (push (cons lpx margin-top) polygon) - (push (cons lpx (- height margin-bottom)) polygon))) - (let ((id (format "gradient-%d" plot-number))) - (when gradient - (eplot--gradient svg id 'linear - (eplot--stops (eplot--vs 'from gradient) - (eplot--vs 'to gradient)) - (eplot--vs 'direction gradient))) - (if (eq style 'curve) - (apply #'svg-path svg - (nconc - (cl-loop - with points = (cl-coerce - (nreverse polygon) 'vector) - for i from 0 upto (1- (length points)) - collect - (cond - ((zerop i) - `(moveto ((,(car (elt points 0)) . - ,(cdr (elt points 0)))))) - (t - `(curveto - (,(eplot--bezier - (eplot--pv plot 'bezier-factor) - i points)))))) - (and gradient '((closepath)))) - `( :clip-path ,clip-id - :stroke-width ,(eplot--pv plot 'size 1) - :stroke ,(slot-value plot 'color) - ,@(if gradient - `(:gradient ,id) - `(:fill "none")))) - (svg-polygon - svg (nreverse polygon) - :clip-path clip-id - :gradient id - :stroke (slot-value plot 'fill-border-color)))))))) - -(defun eplot--element-size (value plot settings default) - (eplot--vn 'size settings - (if (slot-value plot 'size-factor) - (* value (slot-value plot 'size-factor)) - (or (slot-value plot 'size) default)))) - -(defun eplot--draw-horizontal-bar-chart (svg chart) - (with-slots ( plots chart-color height format - margin-bottom margin-left - min max xs ys - margin-top - x-values x-min x-max - label-font label-font-size label-color - horizontal-label-left horizontal-label-font-size) - chart - (dolist (plot plots) - (cl-loop with values = (slot-value plot 'values) - with stride = (e/ ys (length values)) - with label-height = (eplot--text-height "xx" label-font - label-font-size) - with bar-gap = (* stride 0.1) - for i from 0 - for value in values - for settings = (plist-get value :settings) - for bar-max-width = (eplot--pv plot 'bar-max-width) - for py = (+ margin-top (* i stride)) - for px = (* (e/ (plist-get value :x) x-max) xs) - for color = (eplot--vary-color - (eplot--vs 'color settings (slot-value plot 'color)) - i) - do - (svg-rectangle svg - margin-left - (+ py (e/ bar-gap 2) - (if (and bar-max-width - (< bar-max-width (- stride bar-gap))) - (- (/ stride 2) (/ bar-max-width 2)) - 0)) - px - (if bar-max-width - (min bar-max-width (- stride bar-gap)) - (- stride bar-gap)) - :fill color) - (svg-text svg (eplot--vs 'label settings) - :font-family label-font - :text-anchor "left" - :font-size horizontal-label-font-size - :font-weight 'normal - :fill label-color - :x (or horizontal-label-left 5) - :y (+ py label-height - (/ stride 2))))))) - -(defun eplot--stops (from to) - (append `((0 . ,from)) - (cl-loop for (pct col) on (split-string to "-") by #'cddr - collect (if col - (cons (string-to-number pct) col) - (cons 100 pct))))) - -(defun eplot--gradient (svg id type stops &optional direction) - "Add a gradient with ID to SVG. -TYPE is `linear' or `radial'. - -STOPS is a list of percentage/color pairs. - -DIRECTION is one of `top-down', `bottom-up', `left-right' or `right-left'. -nil means `top-down'." - (svg--def - svg - (apply - #'dom-node - (if (eq type 'linear) - 'linearGradient - 'radialGradient) - `((id . ,id) - (x1 . ,(if (eq direction 'left-right) 1 0)) - (x2 . ,(if (eq direction 'right-left) 1 0)) - (y1 . ,(if (eq direction 'bottom-up) 1 0)) - (y2 . ,(if (eq direction 'top-down) 1 0))) - (mapcar - (lambda (stop) - (dom-node 'stop `((offset . ,(format "%s%%" (car stop))) - (stop-color . ,(cdr stop))))) - stops)))) - -(defun e% (num1 num2) - (let ((factor (max (expt 10 (eplot--decimal-digits num1)) - (expt 10 (eplot--decimal-digits num2))))) - (% (truncate (* num1 factor)) (truncate (* num2 factor))))) - -(defun eplot--decimal-digits (number) - (- (length (replace-regexp-in-string - "0+\\'" "" - (format "%.10f" (- number (truncate number))))) - 2)) - -(defun e/ (&rest numbers) - (if (cl-every #'integerp numbers) - (let ((int (apply #'/ numbers)) - (float (apply #'/ (* 1.0 (car numbers)) (cdr numbers)))) - (if (= int float) - int - float)) - (apply #'/ numbers))) - -(defun eplot--get-ticks (min max height &optional whole) - (let* ((diff (abs (- min max))) - (even (eplot--pleasing-numbers (* (e/ diff height) 10))) - (factor (max (expt 10 (eplot--decimal-digits even)) - (expt 10 (eplot--decimal-digits diff)))) - (fmin (truncate (* min factor))) - (feven (truncate (* factor even))) - start) - (when whole - (setq even 1 - feven factor)) - - (setq start - (cond - ((< min 0) - (+ (floor fmin) - feven - (- (% (floor fmin) feven)) - (- feven))) - (t - (- fmin (% fmin feven))))) - (cl-loop for x from start upto (* max factor) by feven - collect (e/ x factor)))) - -(defun eplot--days-to-time (days) - (days-to-time (- days (time-to-days 0)))) - -(defun eplot--get-date-ticks (start end xs label-font label-font-size - x-label-format &optional skip-until) - (let* ((duration (- end start)) - (limits - (list - (list (/ 368 16) 'date - (lambda (_d) t)) - (list (/ 368 4) 'date - ;; Collect Mondays. - (lambda (decoded) - (= (decoded-time-weekday decoded) 1))) - (list (/ 368 2) 'date - ;; Collect 1st and 15th. - (lambda (decoded) - (or (= (decoded-time-day decoded) 1) - (= (decoded-time-day decoded) 15)))) - (list (* 368 2) 'date - ;; Collect 1st of every month. - (lambda (decoded) - (= (decoded-time-day decoded) 1))) - (list (* 368 4) 'date - ;; Collect every quarter. - (lambda (decoded) - (and (= (decoded-time-day decoded) 1) - (memq (decoded-time-month decoded) '(1 4 7 10))))) - (list (* 368 8) 'date - ;; Collect every half year. - (lambda (decoded) - (and (= (decoded-time-day decoded) 1) - (memq (decoded-time-month decoded) '(1 7))))) - (list 1.0e+INF 'year - ;; Collect every Jan 1st. - (lambda (decoded) - (and (= (decoded-time-day decoded) 1) - (= (decoded-time-month decoded) 1))))))) - ;; First we collect the potential ticks. - (while (or (>= duration (caar limits)) - (and skip-until (>= skip-until (caar limits)))) - (pop limits)) - (let* ((x-ticks (cl-loop for day from start upto end - for time = (eplot--days-to-time day) - for decoded = (decode-time time) - when (funcall (nth 2 (car limits)) decoded) - collect day)) - (count (length x-ticks)) - (print-format (nth 1 (car limits))) - (max-print (eplot--format-value (car x-ticks) print-format - x-label-format)) - (min-spacing (* 1.2 (eplot--text-width max-print label-font - label-font-size)))) - (cond - ;; We have room for every X value. - ((< (* count min-spacing) xs) - (list x-ticks print-format)) - ;; We have to prune X labels, but not grid lines. (We shouldn't - ;; have a grid line more than every 10 pixels.) - ((< (* count 10) xs) - (cond - ((not (cdr limits)) - (eplot--year-ticks - x-ticks xs label-font label-font-size x-label-format)) - ;; The Mondays grid is special, because it doesn't resolve - ;; into any of the bigger limits evenly. - ((= (caar limits) (/ 368 4)) - (let* ((max-print (eplot--format-value - (car x-ticks) print-format x-label-format)) - (min-spacing (* 1.2 (eplot--text-width - max-print label-font label-font-size))) - (weed-factor 2)) - (while (> (* (/ (length x-ticks) weed-factor) min-spacing) xs) - (setq weed-factor (* weed-factor 2))) - (list x-ticks 'date - (cl-loop for val in x-ticks - for i from 0 - collect (list val t (zerop (% i weed-factor))))))) - (t - (pop limits) - (catch 'found - (while limits - (let ((candidate - (cl-loop for day in x-ticks - for time = (eplot--days-to-time day) - for decoded = (decode-time time) - collect (list day t - (not (not - (funcall (nth 2 (car limits)) - decoded))))))) - (setq print-format (nth 1 (car limits))) - (let* ((max-print (eplot--format-value - (car x-ticks) print-format x-label-format)) - (min-spacing (* 1.2 (eplot--text-width - max-print label-font - label-font-size))) - (num-labels (seq-count (lambda (v) (nth 2 v)) - candidate))) - (when (and (not (zerop num-labels)) - (< (* num-labels min-spacing) xs)) - (throw 'found (list x-ticks print-format candidate))))) - (pop limits)) - (eplot--year-ticks - x-ticks xs label-font label-font-size x-label-format))))) - ;; We have to reduce both grid lines and labels. - (t - (eplot--get-date-ticks start end xs label-font label-font-size - x-label-format (caar limits))))))) - -(defun eplot--year-ticks (x-ticks xs label-font label-font-size x-label-format) - (let* ((year-ticks (mapcar (lambda (day) - (decoded-time-year - (decode-time (eplot--days-to-time day)))) - x-ticks)) - (xv (eplot--compute-x-ticks - xs year-ticks 'year x-label-format label-font label-font-size))) - (let ((tick-step (car xv)) - (label-step (cadr xv))) - (list x-ticks 'year - (cl-loop for year in year-ticks - for val in x-ticks - collect (list val - (zerop (% year tick-step)) - (zerop (% year label-step)))))))) - -(defun eplot--get-time-ticks (start end xs label-font label-font-size - x-label-format - &optional skip-until) - (let* ((duration (- end start)) - (limits - (list - (list (* 2 60) 'time - (lambda (_d) t)) - (list (* 2 60 60) 'time - ;; Collect whole minutes. - (lambda (decoded) - (zerop (decoded-time-second decoded)))) - (list (* 3 60 60) 'minute - ;; Collect five minutes. - (lambda (decoded) - (zerop (% (decoded-time-minute decoded) 5)))) - (list (* 4 60 60) 'minute - ;; Collect fifteen minutes. - (lambda (decoded) - (and (zerop (decoded-time-second decoded)) - (memq (decoded-time-minute decoded) '(0 15 30 45))))) - (list (* 8 60 60) 'minute - ;; Collect half hours. - (lambda (decoded) - (and (zerop (decoded-time-second decoded)) - (memq (decoded-time-minute decoded) '(0 30))))) - (list 1.0e+INF 'hour - ;; Collect whole hours. - (lambda (decoded) - (and (zerop (decoded-time-second decoded)) - (zerop (decoded-time-minute decoded)))))))) - ;; First we collect the potential ticks. - (while (or (>= duration (caar limits)) - (and skip-until (>= skip-until (caar limits)))) - (pop limits)) - (let* ((x-ticks (cl-loop for time from start upto end - for decoded = (decode-time time) - when (funcall (nth 2 (car limits)) decoded) - collect time)) - (count (length x-ticks)) - (print-format (nth 1 (car limits))) - (max-print (eplot--format-value (car x-ticks) print-format - x-label-format)) - (min-spacing (* (+ (length max-print) 2) (e/ label-font-size 2)))) - (cond - ;; We have room for every X value. - ((< (* count min-spacing) xs) - (list x-ticks print-format)) - ;; We have to prune X labels, but not grid lines. (We shouldn't - ;; have a grid line more than every 10 pixels.) - ;; If we're plotting just seconds, then just weed out some seconds. - ((and (< (* count 10) xs) - (= (caar limits) (* 2 60))) - (let ((xv (eplot--compute-x-ticks - xs x-ticks 'time x-label-format label-font label-font-size))) - (let ((tick-step (car xv)) - (label-step (cadr xv))) - (list x-ticks 'time - (cl-loop for val in x-ticks - collect (list val - (zerop (% val tick-step)) - (zerop (% val label-step)))))))) - ;; Normal case for pruning labels, but not grid lines. - ((< (* count 10) xs) - (if (not (cdr limits)) - (eplot--hour-ticks x-ticks xs label-font label-font-size - x-label-format) - (pop limits) - (catch 'found - (while limits - (let ((candidate - (cl-loop for val in x-ticks - for decoded = (decode-time val) - collect (list val t - (not (not - (funcall (nth 2 (car limits)) - decoded))))))) - (setq print-format (nth 1 (car limits))) - (let ((min-spacing (* (+ (length max-print) 2) - (e/ label-font-size 2)))) - (when (< (* (seq-count (lambda (v) (nth 2 v)) candidate) - min-spacing) - xs) - (throw 'found (list x-ticks print-format candidate))))) - (pop limits)) - (eplot--hour-ticks x-ticks xs label-font label-font-size - x-label-format)))) - ;; We have to reduce both grid lines and labels. - (t - (eplot--get-time-ticks start end xs label-font label-font-size - x-label-format (caar limits))))))) - -(defun eplot--hour-ticks (x-ticks xs label-font label-font-size - x-label-format) - (let* ((eplot--pleasing-numbers '(1 3 6 12)) - (hour-ticks (mapcar (lambda (time) - (decoded-time-hour (decode-time time))) - x-ticks)) - (xv (eplot--compute-x-ticks - xs hour-ticks 'year x-label-format label-font label-font-size))) - (let ((tick-step (car xv)) - (label-step (cadr xv))) - (list x-ticks 'hour - (cl-loop for hour in hour-ticks - for val in x-ticks - collect (list val - (zerop (% hour tick-step)) - (zerop (% hour label-step)))))))) - -(defun eplot--int (number) - (cond - ((integerp number) - number) - ((= number (truncate number)) - (truncate number)) - (t - number))) - -(defun eplot--pleasing-numbers (number) - (let* ((digits (eplot--decimal-digits number)) - (one (e/ 1 (expt 10 digits))) - (two (e/ 2 (expt 10 digits))) - (five (e/ 5 (expt 10 digits)))) - (catch 'found - (while t - (when (< number one) - (throw 'found one)) - (setq one (* one 10)) - (when (< number two) - (throw 'found two)) - (setq two (* two 10)) - (when (< number five) - (throw 'found five)) - (setq five (* five 10)))))) - -(defun eplot-parse-and-insert (file) - "Parse and insert a file in the current buffer." - (interactive "fEplot file: ") - (let ((default-directory (file-name-directory file))) - (setq-local eplot--current-chart - (eplot--render (with-temp-buffer - (insert-file-contents file) - (eplot--parse-buffer)))))) - -(defun eplot-list-chart-headers () - "Pop to a buffer showing all chart parameters." - (interactive) - (pop-to-buffer "*eplot help*") - (let ((inhibit-read-only t)) - (special-mode) - (erase-buffer) - (insert "The following headers influence the overall\nlook of the chart:\n\n") - (eplot--list-headers eplot--chart-headers) - (ensure-empty-lines 2) - (insert "The following headers are per plot:\n\n") - (eplot--list-headers eplot--plot-headers) - (goto-char (point-min)))) - -(defun eplot--list-headers (headers) - (dolist (header (sort (copy-sequence headers) - (lambda (e1 e2) - (string< (car e1) (car e2))))) - (insert (propertize (capitalize (symbol-name (car header))) 'face 'bold) - "\n") - (let ((start (point))) - (insert (plist-get (cdr header) :doc) "\n") - (when-let ((valid (plist-get (cdr header) :valid))) - (insert "Possible values are: " - (mapconcat (lambda (v) (format "`%s'" v)) valid ", ") - ".\n")) - (indent-rigidly start (point) 2)) - (ensure-empty-lines 1))) - -(defvar eplot--transients - '((("Size" - ("sw" "Width") - ("sh" "Height") - ("sl" "Margin-Left") - ("st" "Margin-Top") - ("sr" "Margin-Right") - ("sb" "Margin-Bottom")) - ("Colors" - ("ca" "Axes-Color") - ("cb" "Border-Color") - ("cc" "Chart-Color") - ("cf" "Frame-Color") - ("cs" "Surround-Color") - ("ct" "Title-Color")) - ("Background" - ("bc" "Background-Color") - ("bg" "Background-Gradient") - ("bi" "Background-Image-File") - ("bv" "Background-Image-Cover") - ("bo" "Background-Image-Opacity"))) - (("General" - ("gt" "Title") - ("gf" "Font") - ("gs" "Font-Size") - ("ge" "Font-Weight") - ("go" "Format") - ("gw" "Frame-Width") - ("gh" "Header-File") - ("gi" "Min") - ("ga" "Max") - ("gm" "Mode") - ("gr" "Reset" eplot--reset-transient) - ("gv" "Save" eplot--save-transient)) - ("Axes, Grid & Legend" - ("xx" "X-Title") - ("xy" "Y-Title") - ("xf" "Label-Font") - ("xz" "Label-Font-Size") - ("xs" "X-Axis-Title-Space") - ("xl" "X-Label-Format") - ("xa" "Y-Label-Format") - ("il" "Grid-Color") - ("io" "Grid-Opacity") - ("ip" "Grid-Position") - ("ll" "Legend") - ("lb" "Legend-Background-Color") - ("lo" "Legend-Border-Color") - ("lc" "Legend-Color")) - ("Plot" - ("ps" "Style") - ("pc" "Color") - ("po" "Data-Column") - ("pr" "Data-format") - ("pn" "Fill-Border-Color") - ("pi" "Fill-Color") - ("pg" "Gradient") - ("pz" "Size") - ("pm" "Smoothing") - ("pb" "Bezier-Factor"))))) - -(defun eplot--define-transients () - (cl-loop for row in eplot--transients - collect (cl-coerce - (cl-loop for column in row - collect - (cl-coerce - (cons (pop column) - (mapcar #'eplot--define-transient column)) - 'vector)) - 'vector))) - -(defun eplot--define-transient (action) - (list (nth 0 action) - (nth 1 action) - ;; Allow explicit commands. - (or (nth 2 action) - ;; Make a command for altering a setting. - (lambda () - (interactive) - (eplot--execute-transient (nth 1 action)))))) - -(defun eplot--execute-transient (action) - (with-current-buffer (or eplot--data-buffer (current-buffer)) - (unless eplot--transient-settings - (setq-local eplot--transient-settings nil)) - (let* ((name (intern (downcase action))) - (spec (assq name (append eplot--chart-headers eplot--plot-headers))) - (type (plist-get (cdr spec) :type))) - ;; Sanity check. - (unless spec - (error "No such header type: %s" name)) - (setq eplot--transient-settings - (append - eplot--transient-settings - (list - (cons - name - (cond - ((eq type 'number) - (read-number (format "Value for %s (%s): " action type))) - ((string-match "color" (downcase action)) - (eplot--read-color (format "Value for %s (color): " action))) - ((string-match "font" (downcase action)) - (eplot--read-font-family - (format "Value for %s (font family): " action))) - ((string-match "gradient" (downcase action)) - (eplot--read-gradient action)) - ((string-match "file" (downcase action)) - (read-file-name (format "File for %s: " action))) - ((eq type 'symbol) - (intern - (completing-read (format "Value for %s: " action) - (plist-get (cdr spec) :valid) - nil t))) - (t - (read-string (format "Value for %s (string): " action)))))))) - (eplot-update-view-buffer)))) - -(defun eplot--read-gradient (action) - (format "%s %s %s %s" - (eplot--read-color (format "%s from color: " action)) - (eplot--read-color (format "%s to color: " action)) - (completing-read (format "%s direction: " action) - '(top-down bottom-up left-right right-left) - nil t) - (completing-read (format "%s position: " action) - '(below above) - nil t))) - -(defun eplot--reset-transient () - (interactive) - (with-current-buffer (or eplot--data-buffer (current-buffer)) - (setq-local eplot--transient-settings nil) - (eplot-update-view-buffer))) - -(defun eplot--save-transient (file) - (interactive "FSave parameters to file: ") - (when (and (file-exists-p file) - (not (yes-or-no-p "File exists; overwrite? "))) - (user-error "Exiting")) - (let ((settings (with-current-buffer (or eplot--data-buffer (current-buffer)) - eplot--transient-settings))) - (with-temp-buffer - (cl-loop for (name . value) in settings - do (insert (capitalize (symbol-name name)) ": " - (format "%s" value) "\n")) - (write-region (point-min) (point-max) file)))) - -(defvar-keymap eplot-control-mode-map - "RET" #'eplot-control-update - "TAB" #'eplot-control-next-field - "C-<tab>" #'eplot-control-next-field - "<backtab>" #'eplot-control-prev-field) - -(define-derived-mode eplot-control-mode special-mode "eplot control" - (setq-local completion-at-point-functions - (cons 'eplot--complete-control completion-at-point-functions)) - (add-hook 'before-change-functions #'eplot--process-text-input-before nil t) - (add-hook 'after-change-functions #'eplot--process-text-value nil t) - (add-hook 'after-change-functions #'eplot--process-text-input nil t) - (setq-local nobreak-char-display nil) - (setq truncate-lines t)) - -(defun eplot--complete-control () - ;; Complete headers names. - (when-let* ((input (get-text-property (point) 'input)) - (name (plist-get input :name)) - (spec (cdr (assq name (append eplot--plot-headers - eplot--chart-headers)))) - (start (plist-get input :start)) - (end (- (plist-get input :end) 2)) - (completion-ignore-case t)) - (skip-chars-backward " " start) - (or - (and (eq (plist-get spec :type) 'symbol) - (lambda () - (let ((valid (plist-get spec :valid))) - (completion-in-region - (save-excursion - (skip-chars-backward "^ " start) - (point)) - end - (mapcar #'symbol-name valid)) - 'completion-attempted))) - (and (string-match "color" (symbol-name name)) - (lambda () - (completion-in-region - (save-excursion - (skip-chars-backward "^ " start) - (point)) - end eplot--colors) - 'completion-attempted)) - (and (string-match "\\bfile\\b" (symbol-name name)) - (lambda () - (completion-in-region - (save-excursion - (skip-chars-backward "^ " start) - (point)) - end (directory-files ".")) - 'completion-attempted)) - (and (string-match "\\bfont\\b" (symbol-name name)) - (lambda () - (completion-in-region - (save-excursion - (skip-chars-backward "^ " start) - (point)) - end - (eplot--font-families)) - 'completion-attempted))))) - -(defun eplot--read-font-family (prompt) - "Prompt for a font family, possibly offering autocomplete." - (let ((families (eplot--font-families))) - (if families - (completing-read prompt families) - (read-string prompt)))) - -(defun eplot--font-families () - (when (executable-find "fc-list") - (let ((fonts nil)) - (with-temp-buffer - (call-process "fc-list" nil t nil ":" "family") - (goto-char (point-min)) - (while (re-search-forward "^\\([^,\n]+\\)" nil t) - (push (downcase (match-string 1)) fonts))) - (seq-uniq (sort fonts #'string<))))) - -(defun eplot-control-next-input () - "Go to the next input field." - (interactive) - (when-let ((match (text-property-search-forward 'input))) - (goto-char (prop-match-beginning match)))) - -(defun eplot-control-update () - "Update the chart based on the current settings." - (interactive) - (let ((settings nil)) - (save-excursion - (goto-char (point-min)) - (while-let ((match (text-property-search-forward 'input))) - (when (equal (get-text-property (prop-match-beginning match) 'face) - 'eplot--input-changed) - (let* ((name (plist-get (prop-match-value match) :name)) - (spec (cdr (assq name (append eplot--plot-headers - eplot--chart-headers)))) - (value - (or (plist-get (prop-match-value match) :value) - (plist-get (prop-match-value match) :original-value)))) - (setq value (string-trim (string-replace "\u00A0" " " value))) - (push (cons name - (cl-case (plist-get spec :type) - (number - (string-to-number value)) - (symbol - (intern (downcase value))) - (symbol-list - (mapcar #'intern (split-string (downcase value)))) - (t - value))) - settings))))) - (with-current-buffer eplot--data-buffer - (setq-local eplot--transient-settings (nreverse settings)) - (eplot-update-view-buffer)))) - -(defvar eplot--column-width nil) - -(defun eplot-create-controls () - "Pop to a buffer that lists all parameters and allows editing." - (interactive) - (with-current-buffer (or eplot--data-buffer (current-buffer)) - (let ((settings eplot--transient-settings) - (data-buffer (current-buffer)) - (chart eplot--current-chart) - ;; Find the max width of all the different names. - (width (seq-max - (mapcar (lambda (e) - (length (cadr e))) - (apply #'append - (mapcar #'cdr - (apply #'append eplot--transients)))))) - (transients (mapcar #'copy-sequence - (copy-sequence eplot--transients)))) - (unless chart - (user-error "Must be called from an eplot buffer that has rendered a chart")) - ;; Rearrange the transients a bit for better display. - (let ((size (caar transients))) - (setcar (car transients) (caadr transients)) - (setcar (cadr transients) size)) - (pop-to-buffer "*eplot controls*") - (unless (eq major-mode 'eplot-control-mode) - (eplot-control-mode)) - (setq-local eplot--data-buffer data-buffer - eplot--column-width (+ width 12 2)) - (let ((inhibit-read-only t) - (before-change-functions nil) - (after-change-functions nil)) - (erase-buffer) - (cl-loop for column in transients - for cn from 0 - do - (goto-char (point-min)) - (end-of-line) - (cl-loop - for row in column - do - (if (zerop cn) - (when (not (bobp)) - (insert (format (format "%%-%ds" (+ width 14)) "") - "\n")) - (unless (= (count-lines (point-min) (point)) 1) - (if (eobp) - (progn - (insert (format (format "%%-%ds" (+ width 14)) "") - "\n") - (insert (format (format "%%-%ds" (+ width 14)) "") - "\n") - (forward-line -1) - (end-of-line)) - (forward-line 1) - (end-of-line)))) - ;; If we have a too-long input in the first column, - ;; then go to the next line. - (when (and (> cn 0) - (> (- (point) (pos-bol)) - (+ width 12 2))) - (forward-line 1) - (end-of-line)) - (insert (format (format "%%-%ds" (+ width 14)) - (propertize (pop row) 'face 'bold))) - (if (looking-at "\n") - (forward-line 1) - (insert "\n")) - (cl-loop - for elem in row - for name = (cadr elem) - for slot = (intern (downcase name)) - when (null (nth 2 elem)) - do - (let* ((object (if (assq slot eplot--chart-headers) - chart - (car (slot-value chart 'plots)))) - (value (format "%s" - (or (cdr (assq slot settings)) - (if (not (slot-boundp object slot)) - "" - (or (slot-value object slot) - "")))))) - (end-of-line) - ;; If we have a too-long input in the first column, - ;; then go to the next line. - (when (and (> cn 0) - (> (- (point) (pos-bol)) - (+ width 12 2))) - (forward-line 1) - (end-of-line)) - (when (and (> cn 0) - (bolp)) - (insert (format (format "%%-%ds" (+ width 14)) "") "\n") - (forward-line -1) - (end-of-line)) - (insert (format (format "%%-%ds" (1+ width)) name)) - (eplot--input slot value - (if (cdr (assq slot settings)) - 'eplot--input-changed - 'eplot--input-default)) - (if (looking-at "\n") - (forward-line 1) - (insert "\n"))))))) - (goto-char (point-min))))) - -(defface eplot--input-default - '((t :background "#505050" - :foreground "#a0a0a0" - :box (:line-width 1))) - "Face for eplot default inputs.") - -(defface eplot--input-changed - '((t :background "#505050" - :foreground "white" - :box (:line-width 1))) - "Face for eplot changed inputs.") - -(defvar-keymap eplot--input-map - :full t :parent text-mode-map - "RET" #'eplot-control-update - "TAB" #'eplot-input-complete - "C-a" #'eplot-move-beginning-of-input - "C-e" #'eplot-move-end-of-input - "C-k" #'eplot-kill-input - "C-<tab>" #'eplot-control-next-field - "<backtab>" #'eplot-control-prev-field) - -(defun eplot-input-complete () - "Complete values in inputs." - (interactive) - (cond - ((let ((completion-fail-discreetly t)) - (completion-at-point)) - ;; Completion was performed; nothing else to do. - nil) - ((not (get-text-property (point) 'input)) - (eplot-control-next-input)) - (t - (user-error "No completion in this field")))) - -(defun eplot-move-beginning-of-input () - "Move to the start of the current input field." - (interactive) - (if (= (point) (eplot--beginning-of-field)) - (goto-char (pos-bol)) - (goto-char (eplot--beginning-of-field)))) - -(defun eplot-move-end-of-input () - "Move to the end of the current input field." - (interactive) - (let ((input (get-text-property (point) 'input))) - (if (or (not input) - (= (point) (1- (plist-get input :end)))) - (goto-char (pos-eol)) - (goto-char (1+ (eplot--end-of-field)))))) - -(defun eplot-control-next-field () - "Move to the beginning of the next field." - (interactive) - (let ((input (get-text-property (point) 'input)) - (start (point))) - (when input - (goto-char (plist-get input :end))) - (let ((match (text-property-search-forward 'input))) - (if match - (goto-char (prop-match-beginning match)) - (goto-char start) - (user-error "No next field"))))) - -(defun eplot-control-prev-field () - "Move to the beginning of the previous field." - (interactive) - (let ((input (get-text-property (point) 'input)) - (start (point))) - (when input - (goto-char (plist-get input :start)) - (unless (bobp) - (forward-char -1))) - (let ((match (text-property-search-backward 'input))) - (unless match - (goto-char start) - (user-error "No previous field"))))) - -(defun eplot-kill-input () - "Remove the part of the input after point." - (interactive) - (let ((end (1+ (eplot--end-of-field)))) - (kill-new (string-trim (buffer-substring (point) end))) - (delete-region (point) end))) - -(defun eplot--input (name value face) - (let ((start (point)) - input) - (insert value) - (when (< (length value) 11) - (insert (make-string (- 11 (length value)) ?\u00A0))) - (put-text-property start (point) 'face face) - (put-text-property start (point) 'inhibit-read-only t) - (put-text-property start (point) 'input - (setq input - (list :name name - :size 11 - :is-default (eq face 'eplot--input-default) - :original-value value - :original-face face - :start (set-marker (make-marker) start) - :value value))) - (put-text-property start (point) 'local-map eplot--input-map) - ;; This seems like a NOOP, but redoing the properties like this - ;; somehow makes `delete-region' work better. - (set-text-properties start (point) (text-properties-at start)) - (insert (propertize " " 'face face - 'input input - 'inhibit-read-only t - 'local-map eplot--input-map)) - (plist-put input :end (point-marker)) - (insert " "))) - -(defun eplot--end-of-field () - (- (plist-get (get-text-property (point) 'input) :end) 2)) - -(defun eplot--beginning-of-field () - (plist-get (get-text-property (point) 'input) :start)) - -(defvar eplot--prev-deletion nil) - -(defun eplot--process-text-input-before (beg end) - (message "Before: %s %s" beg end) - (cond - ((= beg end) - (setq eplot--prev-deletion nil)) - ((> end beg) - (setq eplot--prev-deletion (buffer-substring beg end))))) - -(defun eplot--process-text-input (beg end _replace-length) - ;;(message "After: %s %s %s %s" beg end replace-length eplot--prev-deletion) - (when-let ((props (if eplot--prev-deletion - (text-properties-at 0 eplot--prev-deletion) - (if (get-text-property end 'input) - (text-properties-at end) - (text-properties-at beg)))) - (input (plist-get props 'input))) - ;; The action concerns something in the input field. - (let ((buffer-undo-list t) - (inhibit-read-only t) - (size (plist-get input :size))) - (save-excursion - (set-text-properties beg (- (plist-get input :end) 2) props) - (goto-char (1- (plist-get input :end))) - (let* ((remains (- (point) (plist-get input :start) 1)) - (trim (- size remains 1))) - (if (< remains size) - ;; We need to add some padding. - (insert (apply #'propertize (make-string trim ?\u00A0) - props)) - ;; We need to delete some padding, but only delete - ;; spaces at the end. - (setq trim (abs trim)) - (while (and (> trim 0) - (eql (char-after (1- (point))) ?\u00A0)) - (delete-region (1- (point)) (point)) - (cl-decf trim)) - (when (> trim 0) - (eplot--possibly-open-column))))) - ;; We re-set the properties so that they are continguous. This - ;; somehow makes the machinery that decides whether we can kill - ;; a word work better. - (set-text-properties (plist-get input :start) - (1- (plist-get input :end)) props) - ;; Compute what the value is now. - (let ((value (buffer-substring-no-properties - (plist-get input :start) - (plist-get input :end)))) - (when (string-match "\u00A0+\\'" value) - (setq value (substring value 0 (match-beginning 0)))) - (plist-put input :value value))))) - -(defun eplot--possibly-open-column () - (save-excursion - (when-let ((input (get-text-property (point) 'input))) - (goto-char (plist-get input :end))) - (unless (looking-at " *\n") - (skip-chars-forward " ") - (while (not (eobp)) - (let ((text (buffer-substring (point) (pos-eol)))) - (delete-region (point) (pos-eol)) - (forward-line 1) - (if (eobp) - (insert (make-string eplot--column-width ?\s) text "\n") - (forward-char eplot--column-width) - (if (get-text-property (point) 'input) - (forward-line 1) - (insert text) - ;; We have to fix up the markers. - (save-excursion - (let* ((match (text-property-search-backward 'input)) - (input (prop-match-value match))) - (plist-put input :start - (set-marker (plist-get input :start) - (prop-match-beginning match))) - (plist-put input :end - (set-marker (plist-get input :end) - (+ (prop-match-end match) 1)))))))))))) - -(defun eplot--process-text-value (beg _end _replace-length) - (when-let* ((input (get-text-property beg 'input))) - (let ((inhibit-read-only t)) - (when (plist-get input :is-default) - (put-text-property (plist-get input :start) - (plist-get input :end) - 'face - (if (equal (plist-get input :original-value) - (plist-get input :value)) - 'eplot--input-default - 'eplot--input-changed)))))) - -(defun eplot--read-color (prompt) - "Read an SVG color." - (completing-read prompt eplot--colors)) - -(eval `(transient-define-prefix eplot-customize () - "Customize Chart" - ,@(eplot--define-transients))) - -(defun eplot--bezier (factor i points) - (cl-labels ((padd (p1 p2) - (cons (+ (car p1) (car p2)) (+ (cdr p1) (cdr p2)))) - (psub (p1 p2) - (cons (- (car p1) (car p2)) (- (cdr p1) (cdr p2)))) - (pscale (factor point) - (cons (* factor (car point)) (* factor (cdr point))))) - (let* ((start (elt points (1- i))) - (end (elt points i)) - (prev (if (< (- i 2) 0) - start - (elt points (- i 2)))) - (next (if (> (1+ i) (1- (length points))) - end - (elt points (1+ i)))) - (start-control-point - (padd start (pscale factor (psub end prev)))) - (end-control-point - (padd end (pscale factor (psub start next))))) - (list (car start-control-point) - (cdr start-control-point) - (car end-control-point) - (cdr end-control-point) - (car end) - (cdr end))))) - -;;; CSV Parsing Stuff. - -(defun eplot--csv-buffer-p () - (save-excursion - (goto-char (point-min)) - (let ((min 1.0e+INF) - (max -1.0e+INF) - (total 0) - (lines 0)) - (while (not (eobp)) - (let ((this 0)) - (while (search-forward "," (pos-eol) t) - (cl-incf total) - (cl-incf this)) - (forward-line 1) - (cl-incf lines) - (setq min (min min this) - max (max max this)))) - (let ((mid (e/ total lines))) - ;; If we have a comma on each line, and it's fairly evenly - ;; distributed, it's a CSV buffer. - (and (>= min 1) - (< (* mid 0.9) min) - (> (* mid 1.1) max)))))) - -(defun eplot--numericalp (value) - (string-match-p "\\`[-.0-9]*\\'" value)) - -(defun eplot--numberish (value) - (if (or (zerop (length value)) - (not (eplot--numericalp value))) - value - (string-to-number value))) - -(defun eplot--parse-csv-buffer () - (unless (fboundp 'pcsv-parse-buffer) - (user-error "You need to install the pcsv package to parse CSV files")) - (let ((csv (and (fboundp 'pcsv-parse-buffer) - ;; This repeated check is just to silence the byte - ;; compiler. - (pcsv-parse-buffer))) - names) - ;; Check whether the first line looks like a header. - (when (and (length> csv 1) - ;; The second line is all numbers... - (cl-every #'eplot--numericalp (nth 1 csv)) - ;; .. and the first line isn't. - (not (cl-every #'eplot--numericalp (nth 0 csv)))) - (setq names (pop csv))) - (list - (cons 'legend (and names "true")) - (cons :plots - (cl-loop - for column from 1 upto (1- (length (car csv))) - collect - (list (cons :headers - (list - (cons 'name (elt names column)) - (cons 'data-format - (cond - ((cl-every (lambda (e) (<= (length e) 4)) - (mapcar #'car csv)) - "year") - ((cl-every (lambda (e) (= (length e) 8)) - (mapcar #'car csv)) - "date") - (t - "number"))) - (cons 'color (eplot--vary-color "vary" (1- column))))) - (cons - :values - (cl-loop for line in csv - collect (list :x (eplot--numberish (car line)) - :value (eplot--numberish - (elt line column))))))))))) - -(declare-function org-element-parse-buffer "org-element") - -(defun eplot--parse-org-buffer () - (require 'org-element) - (let* ((table (nth 2 (nth 2 (org-element-parse-buffer)))) - (columns (cl-loop for cell in (nthcdr 2 (nth 2 table)) - collect (substring-no-properties (nth 2 cell)))) - (value-column (or (seq-position columns "value") 0)) - (date-column (seq-position columns "date"))) - `((:plots - ((:headers - ,@(and date-column '((data-format . "date")))) - (:values - ,@(cl-loop for row in (nthcdr 4 table) - collect - (let ((cells (cl-loop for cell in (nthcdr 2 row) - collect (substring-no-properties - (nth 2 cell))))) - (list :value (string-to-number (elt cells value-column)) - :x (string-to-number - (replace-regexp-in-string - "[^0-9]" "" (elt cells date-column))) - ))))))))) - -(provide 'eplot) - -;;; eplot.el ends here |
