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.
This commit is contained in:
TEC 2020-07-09 04:47:40 +08:00 committed by Bastien
parent bee7ef6f46
commit 73c99bf42d
1 changed files with 100 additions and 0 deletions

View File

@ -64,6 +64,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"))
@ -182,6 +187,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