From d99a61170bb0ff10b9fc7b99cdc957ec574c1e51 Mon Sep 17 00:00:00 2001 From: TEC Date: Thu, 9 Jul 2020 04:47:40 +0800 Subject: [PATCH 05/11] org-plot.el: add utility functions for range,ticks * lisp/org-plot.el (org-plot/add-options-to-plist): Add the options :ymin :ymax :xmin :xmax, as well as :min and :max as aliases to the y{min,max} options. The :ticks option is also added, for specifying how many ticks should be used. (org--plot/values-stats, org--plot/sensible-tick-num, org--plot/nice-frequency-pick, org--plot/merge-alists, org--plot/item-frequencies, org--plot/prime-factors): New utility functions added to allow for somewhat sensible determination of a :ticks value when none is provided. This turns out to be harder than expected, and so a number of functions are used to attempt to do so. The essence of the method used, is to round values and find their prime decompositions. From this we try to select the most common components to give a reasonable step size. We also add a 'ticks' parameter for manually setting the number of ticks, and (y)min/max parameters similarly. --- lisp/org-plot.el | 100 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/lisp/org-plot.el b/lisp/org-plot.el index 207f5d4af..2a9c0f5bd 100644 --- a/lisp/org-plot.el +++ b/lisp/org-plot.el @@ -63,6 +63,11 @@ Returns the resulting property list." ("map" . :map) ("timeind" . :timeind) ("timefmt" . :timefmt) + ("min" . :ymin) + ("max" . :ymax) + ("ymin" . :ymin) + ("xmax" . :xmax) + ("ticks" . :ticks) ("trans" . :transpose) ("transpose" . :transpose))) (multiples '("set" "line")) @@ -181,6 +186,101 @@ and dependent variables." (setf back-edge "") (setf front-edge "")))) row-vals)) +(defun org--plot/values-stats (nums &optional hard-min hard-max) + "From a list of NUMS return a plist containing some rudamentry statistics on the +values, namely regarding the range." + (let* ((minimum (or hard-min (apply #'min nums))) + (maximum (or hard-max (apply #'max nums))) + (range (- maximum minimum)) + (rangeOrder (ceiling (- 1 (log10 range)))) + (range-factor (expt 10 rangeOrder)) + (nice-min (/ (float (floor (* minimum range-factor))) range-factor)) + (nice-max (/ (float (ceiling (* maximum range-factor))) range-factor))) + `(:min ,minimum :max ,maximum :range ,range + :range-factor ,range-factor + :nice-min ,nice-min :nice-max ,nice-max :nice-range ,(- nice-max nice-min)))) + +(defun org--plot/sensible-tick-num (table &optional hard-min hard-max) + "From a the values in a TABLE of data, attempt to guess an appropriate number of ticks." + (let* ((row-data + (mapcar (lambda (row) (org--plot/values-stats + (mapcar #'string-to-number (cdr row)) + hard-min + hard-max)) table)) + (row-normalised-ranges (mapcar (lambda (r-data) + (let ((val (round (* + (plist-get r-data :range-factor) + (plist-get r-data :nice-range))))) + (if (= (% val 10) 0) (/ val 10) val))) + row-data)) + (range-prime-decomposition (mapcar #'org--plot/prime-factors row-normalised-ranges)) + (weighted-factors (sort (apply #'org--plot/merge-alists #'+ 0 + (mapcar (lambda (factors) (org--plot/item-frequencies factors t)) + range-prime-decomposition)) + (lambda (a b) (> (cdr a) (cdr b)))))) + (apply #'* (org--plot/nice-frequency-pick weighted-factors)))) + +(defun org--plot/nice-frequency-pick (frequencies) + "From a list of frequences, try to sensibly pick a sample of the most frequent." + ;; TODO this mosly works decently, but counld do with some tweaking to work more consistently. + (case (length frequencies) + (1 (list (car (nth 0 frequencies)))) + (2 (if (<= 3 (/ (cdr (nth 0 frequencies)) + (cdr (nth 1 frequencies)))) + (make-list 2 + (car (nth 0 frequencies))) + (list (car (nth 0 frequencies)) + (car (nth 1 frequencies))))) + (t + (let* ((total-count (apply #'+ (mapcar #'cdr frequencies))) + (n-freq (mapcar (lambda (freq) `(,(car freq) . ,(/ (float (cdr freq)) total-count))) frequencies)) + (f-pick (list (car (car n-freq)))) + (1-2-ratio (/ (cdr (nth 0 n-freq)) + (cdr (nth 1 n-freq)))) + (2-3-ratio (/ (cdr (nth 1 n-freq)) + (cdr (nth 2 n-freq)))) + (1-3-ratio (* 1-2-ratio 2-3-ratio)) + (1-val (car (nth 0 n-freq))) + (2-val (car (nth 1 n-freq))) + (3-val (car (nth 2 n-freq)))) + (when (> 1-2-ratio 4) (push 1-val f-pick)) + (when (and (< 1-2-ratio 2-val) + (< (* (apply #'* f-pick) 2-val) 30)) + (push 2-val f-pick)) + (when (and (< 1-3-ratio 3-val) + (< (* (apply #'* f-pick) 3-val) 30)) + (push 3-val f-pick)) + f-pick)))) + +(defun org--plot/merge-alists (function default alist1 alist2 &rest alists) + "Using FUNCTION, combine the elements of all given ALISTS. When an element is +only present in one alist, DEFAULT is used as the second argument for the FUNCTION." + (when (> (length alists) 0) + (setq alist2 (apply #'org--plot/merge-alists function default alist2 alists))) + (flet ((keys (alist) (mapcar #'car alist)) + (lookup (key alist) (or (cdr (assoc key alist)) default))) + (loop with keys = (union (keys alist1) (keys alist2) :test 'equal) + for k in keys collect + (cons k (funcall function (lookup k alist1) (lookup k alist2)))))) + +(defun org--plot/item-frequencies (values &optional normalise) + "Return an alist indicating the frequency of values in VALUES list." + (let ((normaliser (if normalise (float (length values)) 1))) + (cl-loop for (n . m) in (seq-group-by #'identity values) + collect (cons n (/ (length m) normaliser))))) + +(defun org--plot/prime-factors (value) + "Return the prime decomposition of VALUE, e.g. for 12, '(3 2 2)" + (let ((factors '(1)) (i 1)) + (while (/= 1 value) + (setq i (1+ i)) + (when (eq 0 (% value i)) + (push i factors) + (setq value (/ value i)) + (setq i (1- i)) + )) + (subseq factors 0 -1))) + (defcustom org-plot/gnuplot-script-preamble "" "String or function which provides content to be inserted into the GNUPlot script before the plot command. Not that this is in addition to, not instead of -- 2.28.0