Coverage report: /home/ellis/comp/core/lib/cli/spark.lisp
Kind | Covered | All | % |
expression | 329 | 392 | 83.9 |
branch | 35 | 42 | 83.3 |
Key
Not instrumented
Conditionalized out
Executed
Not executed
Both branches taken
One branch taken
Neither branch taken
1
;;; lib/cli/spark.lisp --- Sparklines
3
;; based on: https://github.com/tkych/cl-spark
5
;; ref: https://www.edwardtufte.com/bboard/q-and-a-fetch-msg?msg_id=0001OR
8
(in-package :cli/spark)
11
(defun string-concat (&rest strings)
12
(with-output-to-string (s)
13
(dolist (string strings)
16
(eval-when (:compile-toplevel :load-toplevel :execute)
17
(defun at-least-two-chars-p (x)
18
(and (simple-vector-p x)
20
(every #'characterp x))))
23
'(and simple-vector (satisfies at-least-two-chars-p)))
25
(declaim (type %ticks *ticks* *vticks*))
27
;;--------------------------------------------------------------------
29
;;--------------------------------------------------------------------
30
;; (vector #\▁ #\▂ #\▃ #\▄ #\▅ #\▆ #\▇ #\█)
32
;; (code-char 9600) => #\UPPER_HALF_BLOCK <=> #\▀
33
;; (code-char 9620) => #\UPPER_ONE_EIGHTH_BLOCK <=> #\▔
34
;; (code-char 9601) => #\LOWER_ONE_EIGHTH_BLOCK <=> #\▁
35
;; (code-char 9602) => #\LOWER_ONE_QUARTER_BLOCK <=> #\▂
36
;; (code-char 9603) => #\LOWER_THREE_EIGHTHS_BLOCK <=> #\▃
37
;; (code-char 9604) => #\LOWER_HALF_BLOCK <=> #\▄
38
;; (code-char 9605) => #\LOWER_FIVE_EIGHTHS_BLOCK <=> #\▅
39
;; (code-char 9606) => #\LOWER_THREE_QUARTERS_BLOCK <=> #\▆
40
;; (code-char 9607) => #\LOWER_SEVEN_EIGHTHS_BLOCK <=> #\▇
41
;; (code-char 9608) => #\FULL_BLOCK <=> #\█
42
;; (code-char 9135) => #\HORIZONTAL_LINE_EXTENSION <=> #\⎯
45
(vector (code-char 9601) (code-char 9602) (code-char 9603)
46
(code-char 9604) (code-char 9605) (code-char 9606)
47
(code-char 9607) (code-char 9608))
49
A simple-vector of characters for representation of sparklines.
50
Default is #(#\▁ #\▂ #\▃ #\▄ #\▅ #\▆ #\▇ #\█).
54
(defvar ternary '(-1 0 1 -1 1 0 -1 1 -1))
56
(spark ternary) => \"▁▄█▁█▄▁█▁\"
58
(let ((*ticks* #(#\_ #\- #\¯)))
59
(spark ternary)) => \"_-¯_¯-_¯_\"
61
(let ((*ticks* #(#\▄ #\⎯ #\▀)))
62
(spark ternary)) => \"▄⎯▀▄▀⎯▄▀▄\"
66
(defun spark (numbers &key min max key)
67
(check-type numbers list)
68
(check-type min (or null real))
69
(check-type max (or null real))
70
(check-type key (or symbol function))
71
(when key (setf numbers (mapcar key numbers)))
75
(RETURN-FROM spark ""))
77
;; Ensure min is the minimum number.
79
(setf min (reduce #'min numbers))
80
(setf numbers (mapcar (lambda (n) (max n min)) numbers)))
82
;; Ensure max is the maximum number.
84
(setf max (reduce #'max numbers))
85
(setf numbers (mapcar (lambda (n) (min n max)) numbers)))
88
(error "max ~S < min ~S." max min))
90
(let ((unit (/ (- max min) (1- (length *ticks*)))))
91
(when (zerop unit) (setf unit 1))
92
(with-output-to-string (s)
93
(loop :for n :in numbers
94
:for nth := (floor (- n min) unit)
95
:do (princ (svref *ticks* nth) s)))))
98
(setf (documentation 'spark 'function) "
99
Generates a sparkline string for a list of real numbers.
101
Usage: SPARK <numbers> &key <min> <max> <key>
103
* <numbers> ::= <list> of <real-number>
104
* <min> ::= { <null> | <real-number> }, default is NIL
105
* <max> ::= { <null> | <real-number> }, default is NIL
106
* <key> ::= <function>
109
* <min> ~ lower bound of output.
110
NIL means the minimum value of the data.
111
* <max> ~ upper bound of output.
112
NIL means the maximum value of the data.
113
* <key> ~ function for preparing data.
117
(spark '(1 0 1 0)) => \"█▁█▁\"
118
(spark '(1 0 1 0 0.5)) => \"█▁█▁▄\"
119
(spark '(1 0 1 0 -1)) => \"█▄█▄▁\"
121
(spark '(0 30 55 80 33 150)) => \"▁▂▃▅▂█\"
122
(spark '(0 30 55 80 33 150) :min -100) => \"▃▄▅▆▄█\"
123
(spark '(0 30 55 80 33 150) :max 50) => \"▁▅██▅█\"
124
(spark '(0 30 55 80 33 150) :min 30 :max 80) => \"▁▁▄█▁█\"
126
(spark '(0 1 2 3 4 5 6 7 8) :key (lambda (x) (sin (* x pi 1/4))))
128
(spark '(0 1 2 3 4 5 6 7 8) :key (lambda (x) (cos (* x pi 1/4))))
131
For more examples, see cl-spark/spark-test.lisp
135
;;--------------------------------------------------------------------
137
;;--------------------------------------------------------------------
138
;; #(#\▏ #\▎ #\▍ #\▌ #\▋ #\▊ #\▉ #\█)
140
;; (code-char 9615) => #\LEFT_ONE_EIGHTH_BLOCK <=> #\▏
141
;; (code-char 9614) => #\LEFT_ONE_QUARTER_BLOCK <=> #\▎
142
;; (code-char 9613) => #\LEFT_THREE_EIGHTHS_BLOCK <=> #\▍
143
;; (code-char 9612) => #\LEFT_HALF_BLOCK <=> #\▌
144
;; (code-char 9611) => #\LEFT_FIVE_EIGHTHS_BLOCK <=> #\▋
145
;; (code-char 9610) => #\LEFT_THREE_QUARTERS_BLOCK <=> #\▊
146
;; (code-char 9609) => #\LEFT_SEVEN_EIGHTHS_BLOCK <=> #\▉
147
;; (code-char 9608) => #\FULL_BLOCK <=> #\█
148
;; (code-char 9616) => #\RIGHT_HALF_BLOCK <=> #\▐
149
;; (code-char 9621) => #\RIGHT_ONE_EIGHTH_BLOCK <=> #\▕
152
(vector (code-char 9615) (code-char 9614) (code-char 9613)
153
(code-char 9612) (code-char 9611) (code-char 9610)
154
(code-char 9609) (code-char 9608))
156
A simple-vector of characters for representation of vartical
157
sparklines. Default is #(#\▏ #\▎ #\▍ #\▌ #\▋ #\▊ #\▉ #\█).
161
;; Japan GDP growth rate, annal
162
;; see. http://data.worldbank.org/indicator/NY.GDP.MKTP.KD.ZG
163
(defparameter growth-rate
164
'((2007 . 2.192186) (2008 . -1.041636) (2009 . -5.5269766)
165
(2010 . 4.652112) (2011 . -0.57031655) (2012 . 1.945)))
167
(vspark growth-rate :key #'cdr :labels (mapcar #'car growth-rate))
170
-5.5269766 -0.4374323 4.652112
171
˫---------------------+---------------------˧
172
2007 ██████████████████████████████████▏
173
2008 ███████████████████▊
175
2010 ████████████████████████████████████████████
176
2011 █████████████████████▉
177
2012 █████████████████████████████████▏
180
(let ((*vticks* #(#\- #\0 #\+)))
181
(vspark growth-rate :key (lambda (y-r) (float-sign (cdr y-r)))
182
:labels (mapcar #'car growth-rate)
195
(defun vspark (numbers &key min max key (size 50) labels title (scale? t) (newline? t))
196
(check-type numbers list)
197
(check-type min (or null real))
198
(check-type max (or null real))
199
(check-type key (or symbol function))
200
(check-type size (integer 1 *))
201
(check-type labels list)
203
(when key (setf numbers (mapcar key numbers)))
207
(RETURN-FROM vspark ""))
209
;; Ensure min is the minimum number.
211
(setf min (reduce #'min numbers))
212
(setf numbers (mapcar (lambda (n) (max n min)) numbers)))
214
;; Ensure max is the maximum number.
216
(setf max (reduce #'max numbers))
217
(setf numbers (mapcar (lambda (n) (min n max)) numbers)))
220
(cond ((< max min) (error "max ~S < min ~S." max min))
221
((= max min) (incf max)) ; ensure all bars are in min.
224
(let ((max-lengeth-label nil))
226
;; Ensure num labels equals to num numbers.
227
(let ((diff (- (length numbers) (length labels))))
229
;; Add padding lacking labels not to miss data.
230
(setf labels (append labels (loop :repeat diff :collect ""))))
232
;; Remove superfluous labels to remove redundant spaces.
233
(setf labels (butlast labels (abs diff))))
235
;; Find max-lengeth-label.
236
(setf max-lengeth-label
241
(length (format nil "~A" label))))))
242
;; Canonicalize labels.
243
(let* ((control-string (format nil "~~~D,,@A " max-lengeth-label)))
245
(mapcar (lambda (label) (format nil control-string label))
247
;; Reduce size for max-lengeth-label.
248
;; * 1 is space between label and bar
249
;; * ensure minimum size 1
250
(setf size (max 1 (- size 1 max-lengeth-label))))
252
(let* ((num-content-ticks (1- (length *vticks*)))
253
(unit (/ (- max min) (* size num-content-ticks)))
255
(when (zerop unit) (setf unit 1))
257
(loop :for n :in numbers
259
:do (when labels (push (nth i labels) result))
260
(push (generate-bar n unit min max num-content-ticks)
262
:finally (setf result (nreverse result)))
265
(awhen (generate-scale min max size max-lengeth-label)
269
(awhen (generate-title title size max-lengeth-label)
273
(apply #'string-concat (push #.(format nil "~%") result))
274
(string-right-trim '(#\Newline)
275
(apply #'string-concat result))))))
278
(setf (documentation 'vspark 'function) "
279
Generates a vartical sparkline string for a list of real numbers.
281
Usage: VSPARK <numbers> &key <min> <max> <key> <size>
282
<labels> <title> <scale?> <newline?>
284
* <numbers> ::= <list> of <real-number>
285
* <min> ::= { <null> | <real-number> }, default is NIL
286
* <max> ::= { <null> | <real-number> }, default is NIL
287
* <key> ::= <function>
288
* <size> ::= <integer 1 *>, default is 50
289
* <labels> ::= <list>
290
* <title> ::= <object>, default is NIL
291
* <scale?> ::= <generalized-boolean>, default is T
292
* <newline?> ::= <generalized-boolean>, default is T
295
* <min> ~ lower bound of output.
296
NIL means the minimum value of the data.
297
* <max> ~ upper bound of output.
298
NIL means the maximum value of the data.
299
* <key> ~ function for preparing data.
300
* <size> ~ maximum number of output columns (contains label).
301
* <labels> ~ labels for data.
302
* <title> ~ If title is too big for size, it is not printed.
303
* <scale?> ~ If T, output graph with scale for easy to see.
304
If string length of min and max is too big for size,
305
the scale is not printed.
306
* <newline?> ~ If T, output graph with newlines for easy to see.
311
;; Life expectancy by WHO region, 2011, bothsexes
312
;; see. http://apps.who.int/gho/data/view.main.690
313
(defvar life-expectancies '((\"Africa\" 56)
315
(\"South-East Asia\" 67)
317
(\"Eastern Mediterranean\" 68)
318
(\"Western Pacific\" 76)
321
(vspark life-expectancies :key #'second :scale? nil :newline? nil)
324
██████████████████████████████████████████████████
325
███████████████████████████▌
326
██████████████████████████████████████████████████
327
██████████████████████████████▏
328
██████████████████████████████████████████████████
329
███████████████████████████████████▏\"
331
(vspark life-expectancies :min 50 :max 80
333
:labels (mapcar #'first life-expectancies)
334
:title \"Life Expectancy\")
339
˫------------+-------------˧
341
Americans ████████████████████████▎
342
South-East Asia ███████████████▉
343
Europe ████████████████████████▎
344
Eastern Mediterranean ████████████████▊
345
Western Pacific ████████████████████████▎
346
Global ██████████████████▋
349
(vspark '(0 1 2 3 4 5 6 7 8) :key (lambda (x) (sin (* x pi 1/4)))
365
(vspark '(0 1 2 3 4 5 6 7 8) :key (lambda (x) (sin (* x pi 1/4)))
382
(vspark '(0 1 2 3 4 5 6 7 8) :key (lambda (x) (sin (* x pi 1/4)))
397
For more examples, see cl-spark/spark-test.lisp
400
(defun generate-bar (number unit min max num-content-ticks)
402
(units frac) (floor (- number min) (* unit num-content-ticks))
403
(with-output-to-string (s)
404
(let ((most-tick (svref *vticks* num-content-ticks)))
405
(dotimes (i units) (princ most-tick s))
406
(unless (= number max)
407
;; max number need not frac.
408
;; if number = max, then always frac = 0.
409
(princ (svref *vticks* (floor frac unit))
413
(defun generate-title (title size max-lengeth-label)
414
(let* ((title-string (princ-to-string title))
415
(mid (floor (- (if max-lengeth-label
416
(+ 1 size max-lengeth-label)
418
(length title-string)) 2)))
421
(replace (make-string (if max-lengeth-label
422
(+ 1 size max-lengeth-label)
424
:initial-element #\Space)
425
title-string :start1 mid)))))
427
(defun ensure-non-double-float (x)
428
(if (integerp x) x (float x 0.0)))
431
(princ-to-string (ensure-non-double-float n)))
433
;; (code-char 743) => #\MODIFIER_LETTER_MID_TONE_BAR <=> #\˧
434
;; (code-char 746) => #\MODIFIER_LETTER_YANG_DEPARTING_TONE_MARK <=> #\˫
435
(defun generate-scale (min max size max-lengeth-label)
436
(let* ((min-string (to-string min))
437
(max-string (to-string max))
438
(num-padding (- size (length min-string) (length max-string))))
439
(when (plusp num-padding)
440
(let* ((mid (/ (+ max min) 2))
441
(mid-string (to-string mid))
442
(num-indent (aif max-lengeth-label (1+ it) 0)))
443
(if (and (< (length mid-string) num-padding)
446
;; A. mid exist case:
447
(format nil "~V,0T~V<~A~;~A~;~A~>~
448
~%~V,0T~V,,,'-<~A~;~A~;~A~>~%"
449
num-indent size min-string mid-string max-string
450
num-indent size #.(code-char 747) #\+ #.(code-char 743))
451
;; B. no mid exist case:
452
(format nil "~V,0T~V<~A~;~A~>~
453
~%~V,0T~V,,,'-<~A~;~A~>~%"
454
num-indent size min-string max-string
455
num-indent size #.(code-char 747) #.(code-char 743)))))))