Sparkly Stats — Stats and sparks from daily data (Emacs package)
Below you find the latest version of (1) the package's README and (2) its main source file.
For the git repository and issue tracker, see the project's page on sr.ht.
For more packages, see Software.
Fonts matter
The monospace font that you choose can make a big difference on how your sparks look. Try them out.
See details about it in Sparkly's README (M-x sparkly-see-readme).
In browsers, the space between lines may look particularly large.
Overview
Give Sparkly Stats a date range and the values for each date and it gives you statistics and sparks.
See the examples in sparkly-stats-report.
Installation
See my page Software for the most up-to-date instructions on how to download and install any of my Emacs packages.
Having downloaded and installed the package and its dependencies, adapt the configurations below to your init.el file.
(use-package sparkly-stats :demand t)
Alternatively, if you don’t have ‘use-package’:
(require 'sparkly-stats)
Summary of callables
Here's an overview of this package's callables:
| Function | Summary |
|---|---|
| sparkly-stats-report | Given an org table, return stats and sparks. |
| sparkly-stats-seq->ht | Make hash table using starting date BEG and sequence SEQ. |
| sparkly-stats-string-paste | Paste STRINGS side-by-side separated by string SEP. |
| sparkly-stats-see-readme | Open sparkly-stats's README.org file. |
| sparkly-stats-see-news | See the News in sparkly-stats's README.org file. |
They're described in more detail below.
Functions
Report
sparkly-stats-report (&optional key-1 value-1 key-2 value-2 ...)
Given an org table, return stats and sparks.
Optional key–value pairs ARGS may be passed.
The following KEYS are available:
:tbl, :num,
:beg, :end,
:frq-args, :rng-args,
:wdf-args, :mdf-args,
:wkf-args, :mof-args
If the value of :tbl is nil, look for an org table at point and
insert the output after it (useful mostly for interactive use).
Otherwise, the value should refer to a key–value collection, and the
function returns the output as a string.
The key–value collection can be any reasonable format supported by xht,
namely: hash table, list of lists, Org table string, JSON string, TSV,
CSV, SSV, key–value lines, alist, or plist.
The keys must be dates.
Accepted formats are ISO 8601 (without time) and Org timestamps.
You can also refer to a named table by passing an org-sbe sexp:
| Date | Flanges |
|---|---|
| 5 | |
| 5 | |
| 13 | |
| 2 |
(sparkly-stats-report :tbl (org-sbe my-flanges))
[2042-04-18 Fri]--[2042-04-22 Tue] (5 days) mode median average sum 5 5 5.0 25 ▅ ▅▅_█▂ b e ▁_▁__▂_______▁ 0 13 ▅ █▂__▅▅_ MTWTFSS
If the value of :beg is:
- some date, use it for the end of the date range.
- nil, use the last date given by the table.
If the value of :end is:
- 'today, make today the end of the date range.
- some date, use it for the end of the date range.
- nil, use the last date given by the table.
If a date is passed for :beg and/or :end, it must be a string in
YYYY-MM-DD format (e.g. "2042-04-29").
The KEY :num is the report number. It specifies what to show. If nil,
use the value of customizable variable sparkly-stats-report-default,
which defaults to 2. Otherwise look that number up in the variables
sparkly-stats-report-alist-preset, sparkly-stats-report-alist-user.
And :rng-args, :frq-args, :wdf-args, :mdf-args, :wkf-args, and
:mof-args are all sparkly-vs arguments, each of which a list, which
could be passed to each of the spark blocks of the output to control
their vertical size:
| Key | Sparks for... |
|---|---|
:rng-args |
date range |
:frq-args |
frequency |
:wdf-args |
day of week frequency |
:mdf-args |
day of month frequency |
:wkf-args |
ISO week frequency |
:mof-args |
month frequency |
;;;; Common usage (sparkly-stats-report :num 1 :tbl "| Date | Flanges | |------------------+---------| | [2042-04-18 Fri] | 5 | | [2042-04-19 Sat] | 5 | | [2042-04-21 Mon] | 13 | | [2042-04-22 Tue] | 2 |") => "\ [2042-04-18 Fri]--[2042-04-22 Tue] (5 days) mode median average sum 5 5 5.0 25 ▅ ▅▅_█▂ b e ▁_▁__▂_______▁ 0 13 " ;;;; Tweak vertical stretching (sparkly-stats-report :num 1 :rng-args '(:lns 4) :frq-args '(:fac .5) :tbl "| Date | Flanges | |------------------+---------| | [2042-04-18 Fri] | 5 | | [2042-04-19 Sat] | 5 | | [2042-04-21 Mon] | 13 | | [2042-04-22 Tue] | 2 |") => "\ [2042-04-18 Fri]--[2042-04-22 Tue] (5 days) mode median average sum 5 5 5.0 25 █ █ ▄▄ █ ██_█▅ b e ▄_▄__█_______▄ 0 13 " ;;;; Larger example, with latest dates first (sparkly-stats-report :num 1 :tbl "| Date | Flanges | |------------------+---------| | [2042-05-03 Sat] | 5 | | [2042-05-02 Fri] | 42 | | [2042-05-01 Thu] | 14 | | [2042-04-30 Wed] | 42 | | [2042-04-27 Sun] | 42 | | [2042-04-26 Sat] | 14 | | [2042-04-25 Fri] | 27 | | [2042-04-24 Thu] | 42 | | [2042-04-22 Tue] | 2 | | [2042-04-21 Mon] | 13 | | [2042-04-19 Sat] | 5 | | [2042-04-18 Fri] | 5 |") => "\ [2042-04-18 Fri]--[2042-05-03 Sat] (16 days) mode median average sum (0 42) 9 15.81 253 ▂ ▂ ▂ ▂ █ █ █ █ █▃ █ █ █ ██ █ █ █ ▅ ██▆█ █▆█ ▅▅_█▂_████__███▅ beg end ▄_▁__▃_______▁▂____________▁______________▄ 0 42 " ;;;; Same, but restricting the dates range ;;;;; Only the beginning (sparkly-stats-report :num 1 :beg "2042-04-28" :tbl "| Date | Flanges | |------------------+---------| | [2042-05-03 Sat] | 5 | | [2042-05-02 Fri] | 42 | | [2042-05-01 Thu] | 14 | | [2042-04-30 Wed] | 42 | | [2042-04-27 Sun] | 42 | | [2042-04-26 Sat] | 14 | | [2042-04-25 Fri] | 27 | | [2042-04-24 Thu] | 42 | | [2042-04-22 Tue] | 2 | | [2042-04-21 Mon] | 13 | | [2042-04-19 Sat] | 5 | | [2042-04-18 Fri] | 5 |") => "\ [2042-04-28 Mon]--[2042-05-03 Sat] (6 days) mode median average sum (0 42) 9.5 17.17 103 ▂ ▂ █ █ █ █ █ █ █▆█ __███▅ b e ▂____▁________▁___________________________▂ 0 42 " ;;;;; Only the end (sparkly-stats-report :num 1 :end "2042-04-29" :tbl "| Date | Flanges | |------------------+---------| | [2042-05-03 Sat] | 5 | | [2042-05-02 Fri] | 42 | | [2042-05-01 Thu] | 14 | | [2042-04-30 Wed] | 42 | | [2042-04-27 Sun] | 42 | | [2042-04-26 Sat] | 14 | | [2042-04-25 Fri] | 27 | | [2042-04-24 Thu] | 42 | | [2042-04-22 Tue] | 2 | | [2042-04-21 Mon] | 13 | | [2042-04-19 Sat] | 5 | | [2042-04-18 Fri] | 5 |") => "\ [2042-04-18 Fri]--[2042-04-29 Tue] (12 days) mode median average sum 0 5 12.5 150 ▂ ▂ █ █ █▃ █ ██ █ ▅ ██▆█ ▅▅_█▂_████__ beg end ▄_▁__▂_______▁▁____________▁______________▂ 0 42 " ;;;;; Both the beginning and the end (sparkly-stats-report :num 1 :beg "2042-04-22" :end "2042-04-29" :tbl "| Date | Flanges | |------------------+---------| | [2042-05-03 Sat] | 5 | | [2042-05-02 Fri] | 42 | | [2042-05-01 Thu] | 14 | | [2042-04-30 Wed] | 42 | | [2042-04-27 Sun] | 42 | | [2042-04-26 Sat] | 14 | | [2042-04-25 Fri] | 27 | | [2042-04-24 Thu] | 42 | | [2042-04-22 Tue] | 2 | | [2042-04-21 Mon] | 13 | | [2042-04-19 Sat] | 5 | | [2042-04-18 Fri] | 5 |") => "\ [2042-04-22 Tue]--[2042-04-29 Tue] (8 days) mode median average sum 0 8 15.88 127 ▂ ▂ █ █ █▃ █ ██ █ ██▆█ ▂_████__ beg end ▃_▁___________▁____________▁______________▂ 0 42 " ;;;;; Same, but with dates that are outside the table's range (sparkly-stats-report :num 1 :beg "2042-04-01" :end "2042-05-31" :tbl "| Date | Flanges | |------------------+---------| | [2042-05-03 Sat] | 5 | | [2042-05-02 Fri] | 42 | | [2042-05-01 Thu] | 14 | | [2042-04-30 Wed] | 42 | | [2042-04-27 Sun] | 42 | | [2042-04-26 Sat] | 14 | | [2042-04-25 Fri] | 27 | | [2042-04-24 Thu] | 42 | | [2042-04-22 Tue] | 2 | | [2042-04-21 Mon] | 13 | | [2042-04-19 Sat] | 5 | | [2042-04-18 Fri] | 5 |") => "\ [2042-04-01 Tue]--[2042-05-31 Sat] (61 days) mode median average sum 0 0 4.15 253 ▂ ▂ ▂ ▂ █ █ █ █ █▃ █ █ █ ██ █ █ █ ▅ ██▆█ █▆█ _________________▅▅_█▂_████__███▅____________________________ beg end ▁ █ █ █ █ █ █_▁__▃_______▁▂____________▁______________▄ 0 42 " ;;;;; Range of one month, shown wrapped in the middle ;; (requires sparkly 0.3.0) (sparkly-stats-report :num 1 :beg "2042-04-16" :end "2042-05-15" :rng-args '(:wrp 15) :tbl "| Date | Flanges | |------------------+---------| | [2042-05-03 Sat] | 5 | | [2042-05-02 Fri] | 42 | | [2042-05-01 Thu] | 14 | | [2042-04-30 Wed] | 42 | | [2042-04-27 Sun] | 42 | | [2042-04-26 Sat] | 14 | | [2042-04-25 Fri] | 27 | | [2042-04-24 Thu] | 42 | | [2042-04-22 Tue] | 2 | | [2042-04-21 Mon] | 13 | | [2042-04-19 Sat] | 5 | | [2042-04-18 Fri] | 5 |") => "\ [2042-04-16 Wed]--[2042-05-15 Thu] (30 days) mode median average sum 0 0 8.43 253 ▂ ▂ ▂ █ █ █ █▃ █ █ ██ █ █ ▅ ██▆█ █ __▅▅_█▂_████__█ ▂ █ █ █ ▆█ ██▅____________ ▂ █ █_▁__▃_______▁▂____________▁______________▄ 0 42 " ;;;;; Range of one month, using alternative bars ;; (requires sparkly 0.3.0) (let ((sparkly-bars "_░▒▓█")) (sparkly-stats-report :num 1 :beg "2042-04-16" :end "2042-05-15" :tbl "| Date | Flanges | |------------------+---------| | [2042-05-03 Sat] | 5 | | [2042-05-02 Fri] | 42 | | [2042-05-01 Thu] | 14 | | [2042-04-30 Wed] | 42 | | [2042-04-27 Sun] | 42 | | [2042-04-26 Sat] | 14 | | [2042-04-25 Fri] | 27 | | [2042-04-24 Thu] | 42 | | [2042-04-22 Tue] | 2 | | [2042-04-21 Mon] | 13 | | [2042-04-19 Sat] | 5 | | [2042-04-18 Fri] | 5 |")) => "\ [2042-04-16 Wed]--[2042-05-15 Thu] (30 days) mode median average sum 0 0 8.43 253 ▒ ▒ ▒ ▒ █ █ █ █ █ █ █ █ █ █ █ █ █▓ █ █ █ ██ █ █ █ ██ █ █ █ ░ ██▒█ █▒█ █ ████ ███ ░░ █ ████ ███░ __██_█▒_████__████____________ beg end ▒ █ █ █ █_░__▓_______░▒____________░______________█ 0 42 " ;;;; Different input formats — :tbl is flexible! ;;;;; Other date formats are accepted (sparkly-stats-report :num 1 :tbl "| Date | Flanges | |------------------------+---------| | 2042-04-18 | 5 | | [2042-04-19 Sat] | 5 | | <2042-04-21 Mon> | 13 | | [2042-04-22 Tue 21:42] | 2 |") => "\ [2042-04-18 Fri]--[2042-04-22 Tue] (5 days) mode median average sum 5 5 5.0 25 ▅ ▅▅_█▂ b e ▁_▁__▂_______▁ 0 13 " ;;;;; Other key–value structures are accepted ;;;;;; List of Lists (sparkly-stats-report :num 1 :tbl '(("Date" "Flanges") ("2042-04-18" 5) ("2042-04-19" 5) ("2042-04-21" 13) ("2042-04-22" 2))) => "\ [2042-04-18 Fri]--[2042-04-22 Tue] (5 days) mode median average sum 5 5 5.0 25 ▅ ▅▅_█▂ b e ▁_▁__▂_______▁ 0 13 " ;;;;;; TSV (sparkly-stats-report :num 1 :tbl "\ Date Flanges 2042-04-18 5 2042-04-19 5 2042-04-21 13 2042-04-22 2") => "\ [2042-04-18 Fri]--[2042-04-22 Tue] (5 days) mode median average sum 5 5 5.0 25 ▅ ▅▅_█▂ b e ▁_▁__▂_______▁ 0 13 " ;;;;;; CSV (sparkly-stats-report :num 1 :tbl "\ Date,Flanges 2042-04-18,5 2042-04-19,5 2042-04-21,13 2042-04-22,2") => "\ [2042-04-18 Fri]--[2042-04-22 Tue] (5 days) mode median average sum 5 5 5.0 25 ▅ ▅▅_█▂ b e ▁_▁__▂_______▁ 0 13 " ;;;;;; 2D Hash Table (sparkly-stats-report :num 1 :tbl (h* "2042-04-18" (h* "Date" "2042-04-18" "Flanges" "5") "2042-04-19" (h* "Date" "2042-04-19" "Flanges" "5") "2042-04-21" (h* "Date" "2042-04-21" "Flanges" "13") "2042-04-22" (h* "Date" "2042-04-22" "Flanges" "2"))) => "\ [2042-04-18 Fri]--[2042-04-22 Tue] (5 days) mode median average sum 5 5 5.0 25 ▅ ▅▅_█▂ b e ▁_▁__▂_______▁ 0 13 " ;;;;;; 1D Hash Table (sparkly-stats-report :num 1 :tbl (h* "2042-04-18" 5 "2042-04-19" 5 "2042-04-21" 13 "2042-04-22" 2)) => "\ [2042-04-18 Fri]--[2042-04-22 Tue] (5 days) mode median average sum 5 5 5.0 25 ▅ ▅▅_█▂ b e ▁_▁__▂_______▁ 0 13 " ;;;;;; Association List (sparkly-stats-report :num 1 :tbl '(("2042-04-18" . 5) ("2042-04-19" . 5) ("2042-04-21" . 13) ("2042-04-22" . 2))) => "\ [2042-04-18 Fri]--[2042-04-22 Tue] (5 days) mode median average sum 5 5 5.0 25 ▅ ▅▅_█▂ b e ▁_▁__▂_______▁ 0 13 " ;;;;;; Property List (sparkly-stats-report :num 1 :tbl '("2042-04-18" 5 "2042-04-19" 5 "2042-04-21" 13 "2042-04-22" 2)) => "\ [2042-04-18 Fri]--[2042-04-22 Tue] (5 days) mode median average sum 5 5 5.0 25 ▅ ▅▅_█▂ b e ▁_▁__▂_______▁ 0 13 " ;;;;;; Key–Value Lines (sparkly-stats-report :num 1 ; none of these need to be sorted, by the way: :tbl "\ 2042-04-21 = 13 2042-04-19 = 5 2042-04-18 = 5 2042-04-22 = 2") => "\ [2042-04-18 Fri]--[2042-04-22 Tue] (5 days) mode median average sum 5 5 5.0 25 ▅ ▅▅_█▂ b e ▁_▁__▂_______▁ 0 13 " ;;;; Reports ;;;;; Exclude sparks — report #0 (sparkly-stats-report :num 0 :tbl "| Date | Flanges | |------------------+---------| | [2042-03-23 Sun] | 3 | | [2042-03-23 Sun] | 3 | | [2042-03-21 Fri] | 6 | | [2042-03-20 Thu] | 6 | | [2042-03-19 Wed] | 3 | | [2042-03-18 Tue] | 3 | | [2042-03-11 Tue] | 6 | | [2042-03-10 Mon] | 3 | | [2042-03-09 Sun] | 6 | | [2042-03-08 Sat] | 12 | | [2042-03-07 Fri] | 6 | | [2042-03-06 Thu] | 6 | | [2042-03-05 Wed] | 6 | | [2042-03-04 Tue] | 6 | | [2042-03-03 Mon] | 8 | | [2042-03-02 Sun] | 2 | | [2042-03-01 Sat] | 7 |") => "\ [2042-03-01 Sat]--[2042-03-23 Sun] (23 days) mode median average sum 6 3 3.87 89 " (sparkly-stats-report :num 0 :tbl "| Date | Flanges | |------------------+---------| | [2042-05-03 Sat] | 5 | | [2042-05-02 Fri] | 42 | | [2042-05-01 Thu] | 14 | | [2042-04-30 Wed] | 42 | | [2042-04-27 Sun] | 42 | | [2042-04-26 Sat] | 14 | | [2042-04-25 Fri] | 27 | | [2042-04-24 Thu] | 42 | | [2042-04-22 Tue] | 2 | | [2042-04-21 Mon] | 13 | | [2042-04-19 Sat] | 5 | | [2042-04-18 Fri] | 5 |") => "\ [2042-04-18 Fri]--[2042-05-03 Sat] (16 days) mode median average sum (0 42) 9 15.81 253 " ;;;;; Include weekday frequencies — report #2 (sparkly-stats-report :num 2 :tbl "| Date | Flanges | |------------------+---------| | [2042-03-23 Sun] | 3 | | [2042-03-23 Sun] | 3 | | [2042-03-21 Fri] | 6 | | [2042-03-20 Thu] | 6 | | [2042-03-19 Wed] | 3 | | [2042-03-18 Tue] | 3 | | [2042-03-11 Tue] | 6 | | [2042-03-10 Mon] | 3 | | [2042-03-09 Sun] | 6 | | [2042-03-08 Sat] | 12 | | [2042-03-07 Fri] | 6 | | [2042-03-06 Thu] | 6 | | [2042-03-05 Wed] | 6 | | [2042-03-04 Tue] | 6 | | [2042-03-03 Mon] | 8 | | [2042-03-02 Sun] | 2 | | [2042-03-01 Sat] | 7 |") => "\ [2042-03-01 Sat]--[2042-03-23 Sun] (23 days) mode median average sum 6 3 3.87 89 ▄ ▇▂█▆▆▆▆█▆▃▆______▃▃▆▆_▃ beg end ▇_▁▄__█▁▁___▁ 0 12 ▃ ▃▇▁▄▄█▃ ███████ MTWTFSS " (sparkly-stats-report :num 2 :tbl "| Date | Flanges | |------------------+---------| | [2042-05-03 Sat] | 5 | | [2042-05-02 Fri] | 42 | | [2042-05-01 Thu] | 14 | | [2042-04-30 Wed] | 42 | | [2042-04-27 Sun] | 42 | | [2042-04-26 Sat] | 14 | | [2042-04-25 Fri] | 27 | | [2042-04-24 Thu] | 42 | | [2042-04-22 Tue] | 2 | | [2042-04-21 Mon] | 13 | | [2042-04-19 Sat] | 5 | | [2042-04-18 Fri] | 5 |") => "\ [2042-04-18 Fri]--[2042-05-03 Sat] (16 days) mode median average sum (0 42) 9 15.81 253 ▂ ▂ ▂ ▂ █ █ █ █ █▃ █ █ █ ██ █ █ █ ▅ ██▆█ █▆█ ▅▅_█▂_████__███▅ beg end ▄_▁__▃_______▁▂____________▁______________▄ 0 42 ▂ █ █ ██ ▂██ ▂ ███ █ ███ █ █████ ▅ █████ █▂█████ MTWTFSS " ;;;;; Include weekday and monthly frequencies — report #3 ;; (we also tweak the vertical sizes of these two) (sparkly-stats-report :num 3 :wdf-args '(:fac .05) :mof-args '(:fac .05) :tbl "| Date | Flanges | |------------------+---------| | [2042-05-03 Sat] | 5 | | [2042-05-02 Fri] | 42 | | [2042-05-01 Thu] | 14 | | [2042-04-30 Wed] | 42 | | [2042-04-27 Sun] | 42 | | [2042-04-26 Sat] | 14 | | [2042-04-25 Fri] | 27 | | [2042-04-24 Thu] | 42 | | [2042-04-22 Tue] | 2 | | [2042-04-21 Mon] | 13 | | [2042-04-19 Sat] | 5 | | [2042-04-18 Fri] | 5 |") => "\ [2042-04-18 Fri]--[2042-05-03 Sat] (16 days) mode median average sum (0 42) 9 15.81 253 ▂ ▂ ▂ ▂ █ █ █ █ █▃ █ █ █ ██ █ █ █ ▅ ██▆█ █▆█ ▅▅_█▂_████__███▅ beg end ▄_▁__▃_______▁▂____________▁______________▄ 0 42 ▆ ▁▆█ ▁ ███▂█ ▅▁█████ MTWTFSS ▅ █ █ █ █ █ █ ██ ██ ___██_______ JFMAMJJASOND " ;;;;; Include all frequencies, fit some side by side — report #6 ;; (we also tweak all vertical sizes) (sparkly-stats-report :num 6 :wkf-args '(:fac .05) :mof-args '(:fac .05) :tbl "| Date | Flanges | |------------------+---------| | [2042-05-03 Sat] | 5 | | [2042-05-02 Fri] | 42 | | [2042-05-01 Thu] | 14 | | [2042-04-30 Wed] | 42 | | [2042-04-27 Sun] | 42 | | [2042-04-26 Sat] | 14 | | [2042-04-25 Fri] | 27 | | [2042-04-24 Thu] | 42 | | [2042-04-22 Tue] | 2 | | [2042-04-21 Mon] | 13 | | [2042-04-19 Sat] | 5 | | [2042-04-18 Fri] | 5 |") => "\ [2042-04-18 Fri]--[2042-05-03 Sat] (16 days) mode median average sum (0 42) 9 15.81 253 ▂ ▂ ▂ ▂ █ █ █ █ █▃ █ █ █ ██ █ █ █ ▅ ██▆█ █▆█ ▅▅_█▂_████__███▅ beg end ▄_▁__▃_______▁▂____________▁______________▄ 0 42 ▅ █ █ █ █ ▂ ▂ ▂ ▂ █ █ █ █ █ █ █ █▃ █ █ ██ █ ██ █ █ ██ ▆█ ▅ ██▆█ █ ___██_______ ██▅______________▅▅_█▂_████__█_ ▂ JFMAMJJASOND d01 d31 █ █ █ ██ █▁ ▂██ ▂ ██ ███ █ ██ ███ █ ██ █████ ██ ▅ █████ _______________▄██___________________________________ █▂█████ W01 W53 MTWTFSS " ;;;;; Same, but building the input with sparkly-stats-seq->ht (let* ((beg "2042-04-18") (seq '(5 5 0 13 2 0 42 27 14 42 0 0 42 14 42 5)) (htb (sparkly-stats-seq->ht beg seq))) (sparkly-stats-report :tbl htb :num 6 :wkf-args '(:fac .05) :mof-args '(:fac .05))) => "\ [2042-04-18 Fri]--[2042-05-03 Sat] (16 days) mode median average sum (0 42) 9 15.81 253 ▂ ▂ ▂ ▂ █ █ █ █ █▃ █ █ █ ██ █ █ █ ▅ ██▆█ █▆█ ▅▅_█▂_████__███▅ beg end ▄_▁__▃_______▁▂____________▁______________▄ 0 42 ▅ █ █ █ █ ▂ ▂ ▂ ▂ █ █ █ █ █ █ █ █▃ █ █ ██ █ ██ █ █ ██ ▆█ ▅ ██▆█ █ ___██_______ ██▅______________▅▅_█▂_████__█_ ▂ JFMAMJJASOND d01 d31 █ █ █ ██ █▁ ▂██ ▂ ██ ███ █ ██ ███ █ ██ █████ ██ ▅ █████ _______________▄██___________________________________ █▂█████ W01 W53 MTWTFSS " ;;;; Coercion to zero ;; Non-numeric values are coerced to zero (sparkly-stats-report :wkf-args '(:fac .05) :mof-args '(:fac .05) :num 1 :tbl "\ | Date | Flanges | |------------------+---------| | [2042-05-03 Sat] | ? | | [2042-05-02 Fri] | 42 | | [2042-05-01 Thu] | 14 | | [2042-04-30 Wed] | nil | | [2042-04-27 Sun] | N/A | | [2042-04-26 Sat] | 14 | | [2042-04-25 Fri] | 27 | | [2042-04-24 Thu] | foo | | [2042-04-22 Tue] | t | | [2042-04-21 Mon] | | | [2042-04-19 Sat] | 5 | | [2042-04-18 Fri] | 5 | ") => "\ [2042-04-18 Fri]--[2042-05-03 Sat] (16 days) mode median average sum 0 0 6.69 107 ▂ █ ▃ █ █ █ █▆ ▆█ ▅▅_____██____██_ beg end ▂ █____▂________▂____________▁______________▁ 0 42 " ;;;; Corner cases ;;;;; Just one day (sparkly-stats-report :num 0 :tbl "| Date | Flanges | |------------------+---------| | [2042-05-01 Thu] | 14 |") => "\ [2042-05-01 Thu] (1 day) mode median average sum 14 14 14.0 14 " (sparkly-stats-report :num 6 :tbl "| Date | Flanges | |------------------+---------| | [2042-05-01 Thu] | 14 |") => "\ [2042-05-01 Thu] (1 day) mode median average sum 14 14 14.0 14 ▆ █ beg ▁ 14 ▆ ▆ ____█_______ █______________________________ JFMAMJJASOND d01 d31 ▆ ▆ _________________█___________________________________ ___█___ W01 W53 MTWTFSS " ;;;; Errors ;;;;; Dates, if passed, must be strings: ISO 8601 or org timestamps (sparkly-stats-report :beg '2042-04-15 :tbl "| Date | Flanges | |------------------+---------| | [2042-04-21 Mon] | 13 | | [2042-04-19 Sat] | 5 | | [2042-04-18 Fri] | 5 |") !!> wrong-type-argument (sparkly-stats-report :beg "20420415" :tbl "| Date | Flanges | |------------------+---------| | [2042-04-21 Mon] | 13 | | [2042-04-19 Sat] | 5 | | [2042-04-18 Fri] | 5 |") !!> error ;;;;; With no :tbl passed — and no org table at point (sparkly-stats-report) !!> error
Make input
sparkly-stats-seq->ht (beg seq)
Make hash table using starting date BEG and sequence SEQ.
BEG is a date string in ISO 8601 or org timestamp format.
SEQ is a list or vector. Each of its values must be either a number or a
string representing a number, or else it'll be coerced to 0.
The resulting hash table can then be passed as input to the :tbl key
of sparkly-stats-report.
(sparkly-stats-seq->ht "2042-11-01" '(2 7 18 5 3 0 1)) H=> (h* "2042-11-01" 2 "2042-11-02" 7 "2042-11-03" 18 "2042-11-04" 5 "2042-11-05" 3 "2042-11-06" 0 "2042-11-07" 1) (sparkly-stats-seq->ht "[2042-11-01 Sat]" ["2" 7 "18" 5 "3" 0 1]) H=> (h* "2042-11-01" 2 "2042-11-02" 7 "2042-11-03" 18 "2042-11-04" 5 "2042-11-05" 3 "2042-11-06" 0 "2042-11-07" 1) (sparkly-stats-seq->ht "[2042-11-01 Sat]" '("2" t "N/A" nil "" :zero 1)) H=> (h* "2042-11-01" 2 "2042-11-02" 0 "2042-11-03" 0 "2042-11-04" 0 "2042-11-05" 0 "2042-11-06" 0 "2042-11-07" 1) (sparkly-stats-seq->ht "2042-11-01" "Not a list or a vector") !!> error
Support
String operations
sparkly-stats-string-paste (sep &rest strings)
Paste STRINGS side-by-side separated by string SEP.
Unlike the UNIX paste command, the strings are aligned by their
respective last (rather than first) lines.
;;;; with two strings (sparkly-stats-string-paste " = " "a\nb\nc" "d\ne\nf") => "\ a = d b = e c = f" (sparkly-stats-string-paste " " "a\nb\nc" "d\ne\nf") => "\ a d b e c f" (sparkly-stats-string-paste " " "a\nb\nc" "d\ne") => "\ a b d c e" (sparkly-stats-string-paste " " "a\nb" "c\nd\ne") => "\ c a d b e" (sparkly-stats-string-paste " " " a\n b\nc" "d\ne") => "\ a b d c e" (sparkly-stats-string-paste " " "a\nb" " c\n d\ne") => "\ c a d b e" ;;;; with three or more strings (sparkly-stats-string-paste ":" "a\n z" "b\n c\nd" " e\n f ") => "\ :b a : c: e z:d : f " (sparkly-stats-string-paste " ←→ " "a\n z" "b\n c\nd" " e\n f ") => "\ ←→ b a ←→ c ←→ e z ←→ d ←→ f " (sparkly-stats-string-paste " " "3\n2\n1" "4\n3\n2\n1" "2\n1") => "\ 4 3 3 2 2 2 1 1 1" (sparkly-stats-string-paste " " "2\n1" "X\n2\n1" "2\n1" "Y\n2\n1" "2\n1") => "\ X Y 2 2 2 2 2 1 1 1 1 1"
See README
sparkly-stats-see-readme (&optional heading narrow)
Open sparkly-stats's README.org file.
Search for the file in sparkly-stats.el's directory.
If found, open it read-only.
If optional argument HEADING is passed, try to navigate to the
heading after opening it. HEADING should be a string.
If optional argument NARROW is non-nil, narrow to that heading.
This argument has no effect if HEADING is nil or not found.
Contributing
See my page Software for information about how to contribute to any of my Emacs packages.
News
0.5.1
Sparkly Stats News
This release has a small fix related to the wrapping of date range sparks.
If you have Democratize, run M-x democratize-library to update the available examples.
0.5.0
Sparkly Stats News
This release brings a new function and more flexibility for input values.
If you have Democratize, run M-x democratize-library to update the available examples.
New features
New functions
sparkly-stats-seq->ht
You can now pass input data as a combination of a start date plus a list or vector of values.
0.4.0
Sparkly Stats News
This release brings several new features and fixes, such as:
- three new reports
- user-customizable reports
- new function: place sparks side by side
- more flexibility in accepted input format
- when using org-sbe, hlines in the table no longer matter
- plain ISO 8601 date is now also accepted
- other org timestamp formats are now accepted
- key–value formats other than org table are now accepted
- when using org-sbe, hlines in the table no longer matter
- and a few bug fixes
If you have Democratize, run M-x democratize-library to update the available examples.
New features
New functions
sparkly-stats-string-paste
Allows you to display sparks side by side, which you can them use to build customizable reports.
Enhancements
More flexible input formats
Other key–value structures are accepted as input as well
Instead of an org table, you can directly pass a list of lists, TSV, CSV, hash table, alist, or plist, for example.
Fixes
Adjusted minimum xht version in Package-Requires
Needed to be at least 2.0 for h-type to return :lines.
Count of days in range has been fixed
An off-by-one error in the count of days in a range was happening when there was change in daylight saving times inside a date range.
Fixed after using a simpler and better function.
Renamed customizable variables
The customizable variable sparkly-report-default was not properly named: it has now been renamed to sparkly-stats-report-default.
Since this mistake was caught before publicly pushing version 0.3.0, marking the previous variable as obsolete seems pointless: just skip directly to this newer version.
0.3.0
Sparkly Stats News
This release brings sparks for weekday frequencies, sparks for month frequencies, and three new report options.
If you have Democratize, run M-x democratize-library to update the available examples.
See also
Other packages
You can integrate sparkly-stats' examples into Helpful or Help buffers with another package of mine called Democratize, which can also integrate examples from xht, sparkly, dash, s, f, and native Emacs libraries.
License
This project follows the REUSE Specification (FAQ), which in turn is built upon SPDX.
Therefore, license and copyright information can be found in:
- each file's comment header, or
- an adjacent file of the same name with the additional extension
.license, or - the
.reuse/dep5file
The full text of the licenses can be found in the LICENSES subdirectory.
sparkly-stats.el
Structure
;;; sparkly-stats.el --- Stats and sparks from daily data -*- lexical-binding: t -*- ;;; Commentary: ;;;; For all the details, please do see the README ;;; Code: ;;;; Libraries ;;;; Symbols from other packages ;;;; Package metadata ;;;; Customizable variables ;;;; Other variables ;;;; Functions ;;;;; Report ;;;;; Make input ;;;;; Support ;;;;;; Parse input ;;;;;; Make output ;;;;;; Make parts of output ;;;;;;; Make header ;;;;;;; Make values ;;;;;;; Make labels ;;;;;;; Make sparks ;;;;;; Statistics ;;;;;; Date operations ;;;;;; String operations ;;;;; See README ;;;; Wrapping up ;;; sparkly-stats.el ends here
Contents
;;; sparkly-stats.el --- Stats and sparks from daily data -*- lexical-binding: t -*- ;; SPDX-FileCopyrightText: © flandrew <https://flandrew.srht.site/listful> ;;--------------------------------------------------------------------------- ;; Author: flandrew ;; Created: 2023-11-12 ;; Updated: 2025-11-02 ;; Keywords: extensions ;; Homepage: <https://flandrew.srht.site/listful/software.html> ;;--------------------------------------------------------------------------- ;; Package-Version: 0.5.1 ;; Package-Requires: ((emacs "25.1")(sparkly "0.2")(xht "2.0")(dash "2.15")(s "1.12")) ;;--------------------------------------------------------------------------- ;; SPDX-License-Identifier: GPL-3.0-or-later ;; This file 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 3 of the License, ;; or (at your option) any later version. ;; ;; This file is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this file. If not, see <https://www.gnu.org/licenses/>. ;;; Commentary: ;; ;; With Sparkly Stats you can create statistics and sparks from daily data. ;; ;;;; For all the details, please do see the README ;; ;; Open it easily with: ;; (find-file-read-only "README.org") <--- C-x C-e here¹ ;; ;; or from any buffer: ;; M-x sparkly-stats-see-readme ;; ;; or read it online: ;; <https://flandrew.srht.site/listful/sw-emacs-sparkly-stats.html> ;; ;; ¹ or the key that ‘eval-last-sexp’ is bound to, if not C-x C-e. ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Code: ;;;; Libraries (require 's) (require 'xht) ; <--also by the author of this package (require 'dash) (require 'sparkly) ; <--also by the author of this package (require 'lisp-mnt) ; ‘lm-summary’, ‘lm-homepage’, ‘lm-version’, ‘lm-header’ (require 'time-date) ; ‘date-to-time’, ‘time-to-days’, ‘days-between’ ;;;; Symbols from other packages ;; Silence "not known to be defined" compiler warnings (declare-function org-at-table-p "ext:org-table" (&optional table-type)) (declare-function org-table-begin "ext:org-table" (&optional table-type)) (declare-function org-table-end "ext:org-table" (&optional table-type)) (defvar org-ts-regexp-both) ;;;; Package metadata (defvar sparkly-stats--name "Sparkly Stats") (defvar sparkly-stats--dot-el (format "%s.el" (file-name-sans-extension (eval-and-compile (or load-file-name buffer-file-name))))) (defvar sparkly-stats--readme-org (expand-file-name "README.org" (file-name-directory sparkly-stats--dot-el))) (defvar sparkly-stats--summary (lm-summary sparkly-stats--dot-el)) (defvar sparkly-stats--homepage (lm-homepage sparkly-stats--dot-el)) (defvar sparkly-stats--version (lm-with-file sparkly-stats--dot-el (or (lm-header "package-version") (lm-version)))) ;;;; Customizable variables (defgroup sparkly-stats nil (format "%s." sparkly-stats--summary) :group 'extensions :link '(emacs-library-link :tag "Lisp file" "sparkly-stats.el") :link `(file-link :tag "README.org" ,sparkly-stats--readme-org) :link `(url-link :tag "Homepage" ,sparkly-stats--homepage)) (defcustom sparkly-stats-report-default 2 "Default report number to use. When no `:num' is passed to `sparkly-stats-report', use this." :package-version '(sparkly-stats "0.4.0") :type '(choice (const 0) (const 1) (const 2) (const 3) (const 4) (const 5) (const 6))) (defcustom sparkly-stats-report-alist-user '((199 . (format "%s\n %s\n %-8s%-10s%-11s%s\n\nDOW:\n%s" hdr lbl mod med avg sum wdf))) "User-defined reports. Preset reports are listed under `sparkly-stats-report-alist-preset'. If none of these are exactly to your liking, create yours here. This format example (199) shows sparks for only weekday frequencies. The keys must be natural numbers greater than or equal to 100. The values are forms that may use any of these variables: hdr = header mod = mode avg = average lbl = label med = median sum = sum plus variables for sparks of: rng = data range wdf = weekday frequency mof = month frequency frq = frequency wkf = ISO week frequency mdf = day of month frequency" :package-version '(sparkly-stats "0.4.0") :type '(alist :key-type (restricted-sexp :tag "Number" :match-alternatives ((lambda (n) (and (natnump n) (>= n 100))))) :value-type (sexp :tag "Form"))) ;;;; Other variables (defvar sparkly-stats-report-alist-preset (let ((ten " ") (five " ") (f1 "%s\n %s\n %-8s%-10s%-11s%s") (f2 "\n\n%s")) `((0 . (format ,f1 hdr lbl mod med avg sum)) (1 . (format ,(concat f1 f2 f2) hdr lbl mod med avg sum rng frq)) (2 . (format ,(concat f1 f2 f2 f2) hdr lbl mod med avg sum rng frq wdf)) (3 . (format ,(concat f1 f2 f2 f2 f2) hdr lbl mod med avg sum rng frq wdf mof)) (4 . (format ,(concat f1 f2 f2 f2 f2 f2) hdr lbl mod med avg sum rng frq wdf mof mdf)) (5 . (format ,(concat f1 f2 f2 f2 f2 f2 f2) hdr lbl mod med avg sum rng frq wdf mof mdf wkf)) (6 . (format ,(concat f1 f2 f2 f2) hdr lbl mod med avg sum rng frq (--> (sparkly-stats-string-paste ,ten mof mdf) (format "%s\n\n%s" it wkf) (sparkly-stats-string-paste ,five it wdf)))))) "Preset reports. - Report 0: header and basic stats - Report 1: report 0 plus date range sparks and frequency sparks - Report 2: report 1 plus weekday frequency sparks - Report 3: report 2 plus month frequency sparks - Report 4: report 3 plus day of month frequency sparks - Report 5: report 4 plus ISO week frequency sparks - Report 6: report 5 but arranging frequencies side by side") (defvar sparkly-stats--date-iso-rx "^[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}$") ;;;; Functions ;;;;; Report ;;;###autoload (defun sparkly-stats-report (&rest args) "Given an org table, return stats and sparks. Optional key–value pairs ARGS may be passed. The following KEYS are available: `:tbl', `:num', `:beg', `:end', `:frq-args', `:rng-args', `:wdf-args', `:mdf-args', `:wkf-args', `:mof-args' If the value of `:tbl' is nil, look for an org table at point and insert the output after it (useful mostly for interactive use). Otherwise, the value should refer to a key–value collection, and the function returns the output as a string. The key–value collection can be any reasonable format supported by xht, namely: hash table, list of lists, Org table string, JSON string, TSV, CSV, SSV, key–value lines, alist, or plist. The keys must be dates. Accepted formats are ISO 8601 (without time) and Org timestamps. You can also refer to a named table by passing an `org-sbe' sexp: #+name: my-flanges | Date | Flanges | |------------------+---------| | [2042-04-18 Fri] | 5 | | [2042-04-19 Sat] | 5 | | [2042-04-21 Mon] | 13 | | [2042-04-22 Tue] | 2 | #+begin_src emacs-lisp \(sparkly-stats-report :tbl (org-sbe my-flanges)) #+end_src #+begin_example [2042-04-18 Fri]--[2042-04-22 Tue] (5 days) mode median average sum 5 5 5.0 25 ▅ ▅▅_█▂ b e ▁_▁__▂_______▁ 0 13 ▅ █▂__▅▅_ MTWTFSS #+end_example If the value of `:beg' is: - some date, use it for the end of the date range. - nil, use the last date given by the table. If the value of `:end' is: - \\='today, make today the end of the date range. - some date, use it for the end of the date range. - nil, use the last date given by the table. If a date is passed for `:beg' and/or `:end', it must be a string in YYYY-MM-DD format (e.g. \"2042-04-29\"). The KEY `:num' is the report number. It specifies what to show. If nil, use the value of customizable variable `sparkly-stats-report-default', which defaults to 2. Otherwise look that number up in the variables `sparkly-stats-report-alist-preset', `sparkly-stats-report-alist-user'. And `:rng-args', `:frq-args', `:wdf-args', `:mdf-args', `:wkf-args', and `:mof-args' are all `sparkly-vs' arguments, each of which a list, which could be passed to each of the spark blocks of the output to control their vertical size: | Key | Sparks for... | |-------------+------------------------| | `:rng-args' | date range | | `:frq-args' | frequency | | `:wdf-args' | day of week frequency | | `:mdf-args' | day of month frequency | | `:wkf-args' | ISO week frequency | | `:mof-args' | month frequency | \(fn &optional KEY-1 VALUE-1 KEY-2 VALUE-2 ...)" (interactive) (-let* (((&plist :tbl :num :rng-args :frq-args) args) ((&plist :wdf-args :mdf-args :wkf-args :mof-args) args) ((&plist :beg :end :htbl) (apply #'sparkly-stats--parse-input args)) (output (--> (sparkly-stats--make-output beg end htbl num :rng-args rng-args :frq-args frq-args :wdf-args wdf-args :mdf-args mdf-args :wkf-args wkf-args :mof-args mof-args) ;; If no ORGTBL, we're at point. ;; So we add leading colons "by hand". (if tbl it (s-replace "\n" "\n: " (format "\n%s" it))) (format "%s\n" it)))) (if tbl output (save-excursion (-let [(_b . e) (sparkly-stats--orgtbl-get-boundaries)] (goto-char e) (sparkly--smart-insert output)))))) ;;;;; Make input (defun sparkly-stats-seq->ht (beg seq) "Make hash table using starting date BEG and sequence SEQ. BEG is a date string in ISO 8601 or org timestamp format. SEQ is a list or vector. Each of its values must be either a number or a string representing a number, or else it'll be coerced to 0. The resulting hash table can then be passed as input to the `:tbl' key of ‘sparkly-stats-report’." (setq beg (sparkly-stats--date-as-iso beg)) ;; Yes, we could do this in a single pass instead of two. ;; No, it doesn't seem worth it in this case: much more code, less readable, ;; with performance gains imperceptible to the user unless SEQ is enormous. (h--hmap (sparkly-stats--date-add-days beg key) (sparkly-stats--as-number-safe value) (cond ((listp seq) (h<-list seq)) ((vectorp seq) (h<-vector seq)) (:otherwise (error "SEQ must be a list or vector"))))) ;;;;; Support ;;;;;; Parse input (defun sparkly-stats--parse-input (&rest args) "Try to parse input. Return (BEG END HTBL). For ARGS, see `sparkly-stats-report'." (-let [(&plist :tbl :beg :end) args] (let* ((htbl (sparkly-stats--input-as-1d-htbl tbl)) (dates (->> htbl h-keys (-sort #'string<))) (beg (pcase beg ('nil (-> dates car sparkly-stats--date-as-iso)) (_ (-> beg sparkly-stats--date-as-iso)))) (end (pcase end ('today (format-time-string "%F")) ('nil (-> dates last car sparkly-stats--date-as-iso)) (_ (-> end sparkly-stats--date-as-iso))))) (list :beg beg :end end :htbl htbl)))) (defun sparkly-stats--input-as-1d-htbl (&optional obj) "Try to read key–value object OBJ; return it as 1D hash table. Org tables can be detected at point, passed as string, or through `org-sbe'. Other key–value objects may also be passed. For example: alists, plists, and hash tables." (let ((input (or obj (sparkly-stats--orgtbl-get-string)))) (pcase (h-type input) ;; Will happen when using org-sbe. ;; Depending on the presence of hlines, headers may be stripped, ;; so we make sure one is there for conversion to 1D. (:lines (-as-> (->> input h-read-list (remq 'hline) (cons '(k v))) lol (if (eq (h-type lol) :lol) (-> lol h<-lol h-2d->1d sparkly-stats--remap-1d-htbl) (error "Could not read it as list of lists")))) ;; Will happen when using orgtbl string, or with orgtbl at point. ;; Also when input is passed as some other key–value object. (_ (-as-> (h<-it input) htbl (pcase (h-dim htbl) (1 htbl) (2 (h-2d->1d htbl)) (_ (error "Could not parse input"))) (sparkly-stats--remap-1d-htbl htbl)))))) (defun sparkly-stats--remap-1d-htbl (htbl) "Ensure HTBL only has valid keys and values. Keys will be org timestamps (ISO 8601 will be converted). Values will be numbers: numbers remain as is, strings representing numbers are converted, anything else is coerced to 0." (unless (h-1d? htbl) (error "%s is not 1D" htbl)) (h--hmap (cond ((string-match-p org-ts-regexp-both key) ;; Canonicalize timestamp (-> (sparkly-stats--date-org-to-iso key) (sparkly-stats--date-iso-to-org))) ((string-match-p sparkly-stats--date-iso-rx key) (sparkly-stats--date-iso-to-org key))) (sparkly-stats--as-number-safe value) htbl)) (defun sparkly-stats--as-number-safe (&optional obj) "Convert OBJ to number, coercing to 0 when not possible." (condition-case nil (h-as-number obj) (error 0))) (defun sparkly-stats--orgtbl-get-boundaries () "Cons cell with boundaries of org table at point." (require 'org-table) (unless (org-at-table-p) (error "No org table at point")) (cons (org-table-begin) (org-table-end))) (defun sparkly-stats--orgtbl-get-string () "String of org table at point." (require 'org-table) (buffer-substring-no-properties (org-table-begin) (org-table-end))) ;;;;;; Make output (defun sparkly-stats--make-output (beg end htbl &optional num &rest args) "Make output from BEG, END, HTBL, NUM and ARGS. For ARGS, see `sparkly-stats-report'." (unless num (setq num sparkly-stats-report-default)) (let* ((ops `(,@sparkly-stats-report-alist-preset ,@sparkly-stats-report-alist-user)) (sexp (alist-get num ops))) (unless sexp (error "Report #%s was not found" num)) (-let [(&hash :hdr :lbl :mod :med :avg :sum :rng :frq :wdf :mdf :wkf :mof) (apply #'sparkly-stats--make-output-ht beg end htbl args)] (funcall `(lambda (hdr lbl mod med avg sum rng frq wdf mdf wkf mof) (ignore hdr lbl mod med avg sum rng frq wdf mdf wkf mof) ,sexp) hdr lbl mod med avg sum rng frq wdf mdf wkf mof)))) (defun sparkly-stats--make-output-ht (beg end htbl &rest args) "Output hash table given BEG, END, HTBL, and ARGS. This hash table has all values needed for generating reports. For ARGS, see `sparkly-stats-report'." (-let* ((lbl "mode median average sum") (beg (sparkly-stats--date-as-org beg)) (end (sparkly-stats--date-as-org end)) (len (sparkly-stats--date-range-length beg end)) (hdr (sparkly-stats--make-header beg end len)) (rnv (sparkly-stats--make-values 'rn beg end htbl)) (wdv (sparkly-stats--make-values 'wd beg end htbl)) (mdv (sparkly-stats--make-values 'md beg end htbl)) (wkv (sparkly-stats--make-values 'wk beg end htbl)) (mov (sparkly-stats--make-values 'mo beg end htbl)) (stt (sparkly-stats--make-stats rnv)) ((&plist :mod :med :avg :sum) stt) ((&plist :rng-args :wdf-args :wkf-args) args) ((&plist :frq-args :mdf-args :mof-args) args) (rng (sparkly-stats--make-sparks 'rn rnv rng-args)) (frq (sparkly-stats--make-sparks 'fr rnv frq-args)) (wdf (sparkly-stats--make-sparks 'wd wdv wdf-args)) (mdf (sparkly-stats--make-sparks 'md mdv mdf-args)) (wkf (sparkly-stats--make-sparks 'wk wkv wkf-args)) (mof (sparkly-stats--make-sparks 'mo mov mof-args))) (h* :hdr hdr :lbl lbl :mod mod :med med :avg avg :sum sum :rng rng :frq frq :wdf wdf :mdf mdf :wkf wkf :mof mof))) ;;;;;; Make parts of output ;;;;;;; Make header (defun sparkly-stats--make-header (beg end len) "Make header from BEG, END, and LEN." (if (> len 1) (format "%s--%s (%s days)" beg end len) (format "%s (1 day)" beg))) ;;;;;;; Make values (defun sparkly-stats--make-values (type beg end htbl) "Return list of VALUES given TYPE, BEG, END, and HTBL." (let ((full (sparkly-stats--make-fullht beg end htbl))) (if (eq type 'rn) (h-values full) (-let* (((fmt lbl) (pcase type ('wd `("%a" ("Mon" "Tue" "Wed" "Thu" "Fri" "Sat" "Sun"))) ('md `("%d" ,(sparkly-stats--make-numstr-list 31))) ('wk `("%V" ,(sparkly-stats--make-numstr-list 53))) ('mo `("%m" ,(sparkly-stats--make-numstr-list 12))))) (rslt (h-zip-lists lbl (make-list (length lbl) 0)))) (h--each full (h-count-put! rslt (h* (sparkly-stats--date-fmt fmt key) value))) (h-values rslt))))) (defun sparkly-stats--make-numstr-list (n) "Make list of number strings from 1 to N." (--map (format "%02d" it) (number-sequence 1 n))) (defun sparkly-stats--make-fullht (beg end htbl) "Return complete hash table given BEG, END, and HTBL. Complete means that it uses BEG and END for range, and all days in-between have values (which might be zero)." (let* ((len (sparkly-stats--date-range-length beg end)) (full (h-new len)) date vals) (dotimes (it len) (setq date (sparkly-stats--date-add-days beg it) vals (sparkly-stats--as-number-safe (h-get htbl (sparkly-stats--date-as-org date)))) (h-put! full date vals)) full)) ;;;;;;; Make labels (defun sparkly-stats--make-labels (type values args) "Make sparks labels given TYPE and a list of VALUES. ARGS is a list of `sparks-vs' arguments." (pcase type ;; Date range ('rn (let ((len (length values)) (wrp (plist-get args :wrp))) (cond ((and wrp (< wrp len)) "") ((= len 1) "beg") (:otherwise (let* ((rep (- len (if (<= len 7) 2 6))) (spc (s-repeat rep " ")) (lbl (if (<= len 7) "b e" "beg end"))) (s-replace " " spc lbl)))))) ;; Frequency ('fr (let* ((min (-min values)) (max (-max values))) (if (= min max) (format "%s" min) (let* ((digmin (sparkly-stats--num-digits min)) (digmax (sparkly-stats--num-digits max)) (repeat (- max min -1 digmin digmax)) (spaces (s-repeat repeat " "))) (format "%s%s%s" min spaces max))))) ;; Other frequencies ('wd "MTWTFSS") ('md (format "%s%s%s" "d01" (s-repeat 25 " ") "d31")) ('wk (format "%s%s%s" "W01" (s-repeat 47 " ") "W53")) ('mo "JFMAMJJASOND"))) ;;;;;;; Make sparks (defun sparkly-stats--make-sparks (type values args) "Make labeled sparks given TYPE and a list of VALUES. ARGS is a list of `sparks-vs' arguments." (let* ((vls (if (eq type 'fr) (sparkly-stats--rng->frq values) values)) (spk (apply #'sparkly-vs vls args)) (lbl (sparkly-stats--make-labels type values args))) (if (s-blank? lbl) spk (format "%s\n%s" spk lbl)))) (defun sparkly-stats--rng->frq (values) "From date range VALUES to frequency values." (let ((cnt-ht (h-count<-list values)) (counts (number-sequence (-min values) (-max values)))) (--map (h-get cnt-ht it 0) counts))) ;;;;;; Statistics (defun sparkly-stats--make-stats (values) "Return plist of stats given list of VALUES." (list :sum (-sum values) :mod (sparkly-stats--mode values) :min (-min values) :med (sparkly-stats--median values) :max (-max values) :avg (sparkly-stats--average values))) (defun sparkly-stats--average (list &optional precision) "Return the average of LIST of numbers. PRECISION is an integer representing the number of decimal digits to round to. When nil, default to no more than 2." (sparkly-stats--roundf (/ (apply #'+ list) (length list) 1.0) (or precision 2))) (defun sparkly-stats--median (list) "Return the median of LIST of numbers." (let ((len (length list)) (lis (-sort #'< list))) (if (= 0 (% len 2)) (let* ((1st (elt lis (/ len 2))) (2nd (elt lis (1- (/ len 2)))) (sum (+ 1st 2nd)) (avg (/ sum 2.0))) (if (and (integerp 1st) (integerp 2nd) (= 0 (% sum 2))) (round avg) (sparkly-stats--roundf avg 2))) (elt lis (/ (1- len) 2))))) (defun sparkly-stats--mode (list) "Return the mode(s) of LIST. If multimodal, return a list of modes. Otherwise, the mode." (let* ((hcount (h-count<-list list)) (maxval (->> hcount h-values -max)) (modes (->> hcount (h--sel (= value maxval)) h-keys))) (if (cdr modes) (-sort #'< modes) (car modes)))) (defun sparkly-stats--roundf (number precision) "Round a NUMBER to a maximum PRECISION of decimal digits. If NUMBER is an integer, return it as float with one decimal digit. If a float, round it to no more decimal digits than PRECISION." (let ((pow10 (expt 10 precision))) (-> number (* pow10) fround (/ pow10)))) (defun sparkly-stats--num-digits (int) "Return the number of digits of integer INT." (if (= 0 int) 1 (-> int abs (log 10) floor 1+))) ;;;;;; Date operations (defun sparkly-stats--date-range-length (beg end) "How many days are there from ISO dates BEG to END? Notice that it's 1+ the difference: the number of elements in the sequence going from BEG to END." (1+ (abs (days-between beg end)))) (defun sparkly-stats--date-add-days (beg days) "Add DAYS to ISO date BEG." (format-time-string "%F" (-> beg date-to-day (+ days) (- (time-to-days 0)) days-to-time))) (defun sparkly-stats--date-as-org (date) "Return date in org timestamp format." (cond ((string-match-p org-ts-regexp-both date) date) ((string-match-p sparkly-stats--date-iso-rx date) (sparkly-stats--date-iso-to-org date)) (:otherwise (error "Invalid date format: %s" date)))) (defun sparkly-stats--date-as-iso (date) "Return date in ISO 8601 format." (cond ((string-match-p sparkly-stats--date-iso-rx date) date) ((string-match-p org-ts-regexp-both date) (sparkly-stats--date-org-to-iso date)) (:otherwise (error "Invalid date format: %s" date)))) (defun sparkly-stats--date-org-to-iso (date) "Convert org timestamp DATE to ISO 8601." (substring date 1 11)) (defun sparkly-stats--date-iso-to-org (date) "Convert ISO 8601 DATE to org timestamp format." (format "[%s %s]" date (sparkly-stats--date-fmt "%a" date))) (defun sparkly-stats--date-fmt (fmt date) "Convert ISO 8601 DATE to another format" (format-time-string fmt (date-to-time date))) ;;;;;; String operations (defun sparkly-stats-string-paste (sep &rest strings) "Paste STRINGS side-by-side separated by string SEP. Unlike the UNIX paste command, the strings are aligned by their respective last (rather than first) lines." (--reduce-r (sparkly-stats--string-paste-2 sep it acc) strings)) (defun sparkly-stats--string-paste-2 (sep s1 s2) "Helper for `sparkly-stats-string-paste', which see." (let* ((ls1 (s-lines s1)) (ls2 (s-lines s2)) (ln1 (length ls1)) (ln2 (length ls2)) (dlt (- ln1 ln2)) (ls1 `(,@(make-list (if (> dlt 0) 0 (- dlt)) "") ,@ls1)) (ls2 `(,@(make-list (if (> dlt 0) dlt 0) "") ,@ls2)) (len (-map #'length ls1)) (mxw (-max len)) st1 st2 spc) (s-chomp (with-output-to-string (dotimes (i (max ln1 ln2)) (setq st1 (nth i ls1) st2 (nth i ls2) spc (if (< i dlt) "" (concat (make-vector (- mxw (nth i len)) ?\s) sep))) (princ (format "%s%s%s\n" st1 spc st2))))))) ;;;;; See README ;;;###autoload (defun sparkly-stats-see-readme (&optional heading narrow) "Open sparkly-stats's README.org file. Search for the file in sparkly-stats.el's directory. If found, open it read-only. If optional argument HEADING is passed, try to navigate to the heading after opening it. HEADING should be a string. If optional argument NARROW is non-nil, narrow to that heading. This argument has no effect if HEADING is nil or not found." (interactive) (let ((readme sparkly-stats--readme-org)) (if (file-exists-p readme) (let ((pr (make-progress-reporter (format "Opening %s ... " (abbreviate-file-name readme))))) (find-file-read-only readme) (when heading (sparkly-stats--goto-org-heading heading narrow)) (progress-reporter-done pr)) (message "Couldn't find %s's README.org" sparkly-stats--name)))) ;;;###autoload (defun sparkly-stats-see-news () "See the News in sparkly-stats's README.org file." (interactive) (sparkly-stats-see-readme "News" 'narrow) (sparkly-stats--display-org-subtree)) (defun sparkly-stats--display-org-subtree () "Selectively display org subtree." (let ((cmds '( outline-hide-subtree outline-show-children outline-next-heading outline-show-branches))) (and (equal (mapcar #'fboundp cmds) '(t t t t)) (mapc #'funcall cmds)))) (defun sparkly-stats--goto-org-heading (heading &optional narrow) "Navigate to org HEADING and optionally NARROW to it." (let* ((hrx (format "^[*]+ %s" heading)) (pos (save-match-data (save-excursion (save-restriction (widen) (goto-char (point-max)) (re-search-backward hrx nil t 1)))))) (when pos (widen) (goto-char pos) (if (and narrow (fboundp 'org-narrow-to-subtree)) (org-narrow-to-subtree) (recenter-top-bottom 1)) (when (fboundp 'outline-show-subtree) (outline-show-subtree)) (when (fboundp 'org-flag-drawer) (save-excursion (forward-line 1) (org-flag-drawer t)))))) ;;;; Wrapping up (provide 'sparkly-stats) ;; Local Variables: ;; coding: utf-8 ;; indent-tabs-mode: nil ;; sentence-end-double-space: nil ;; outline-regexp: ";;;;* " ;; End: ;;; sparkly-stats.el ends here
📆 2025-01-10