Sparkly — Create multiline sparks ▁▂▆▄▃▇▅ (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.
More details about fonts are available further below.
In browsers, the space between lines may look particularly large.
Overview
With Sparkly you can create multiline sparks such as these ones:
(sparkly-vs '(3 14 15 92 65 35 89 79 32 38 46 26 43 38 32 79 50 28 84 19 71 69 39 93 75 10 58 20 97 49 44 59 23 07 81 64 06)) => "\ ▁ ▄ ▁ ▅ █ █ █ ▄ █ █ ▁ █ █▇ ▇ █ █▃ █ █ █▁ ██ █ █ ▇▅ ██ █ █ ██ ██ █ █ ██ ██ ▂ █ ▃ ██ ██ ██ █▂ █ ██ ██ █ █▁ █ ██ ██ ██ ▆ ▃ ██ █ ██ ██ █ ██▄█ ██ ██▃██ ▆█ █▆ ██ █ ██▇██ █ ████ ██ ████████▂█████▄█ █████ █ ████ ██ ████████████████▃█████ █▄████▇ ██ ▆▇██████████████████████▂███████ ██ ▃████████████████████████████████▇██▆"
which you could also fit in, say, exactly four lines:
(sparkly-vs '(3 14 15 92 65 35 89 79 32 38 46 26 43 38 32 79 50 28 84 19 71 69 39 93 75 10 58 20 97 49 44 59 23 07 81 64 06) :lns 4) => "\ ▆ ▅▂ ▂ ▄ ▇▁ █ ▃ █▅ ██ █ █ ▇▇ ██ ▃ █ ▃ █▅ ██▄██▃▅▇▁▆▅▃██▁█ ██▅██ █ ██▇█ ██ ▁▅▅████████████████▆█████▃█▇█████▂██▂"
or just one, making it a "traditional sparkline":
(sparkly-vs '(3 14 15 92 65 35 89 79 32 38 46 26 43 38 32 79 50 28 84 19 71 69 39 93 75 10 58 20 97 49 44 59 23 07 81 64 06) :lns 1) => "\ _▁▁█▅▃▇▇▃▃▄▂▄▃▃▇▄▂▇▂▆▆▃█▆▁▅▂█▄▄▅▂▁▇▅_"
Parameters can be adjusted. If, for example, we run the following:
(sparkly-vs '(5 10 20)) (sparkly-vs '(5 10 20) :fit t) (sparkly-vs '(5 10 20) :lns 2) (sparkly-vs '(5 10 20) :lns 2 :lim 1) (sparkly-vs '(2 2.5 3) :fac 1)
we get, respectively, these (shown side by side for comparison):
▄ █ ▄█ ▂█ ▄█ █ ███ ▅██ ▆██ ▄██ ▂▄█ ███
See sparkly-vs below for more details.
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 :demand t)
Alternatively, if you don't have use-package:
(require 'sparkly)
Customization
You can globally customize the bars' glyphs with, for example:
(use-package sparkly :demand t :custom ; preset: "_▁▂▃▄▅▆▇█" (default value) ;; could also be: ".▁▂▃▄▅▆▇█" (dot for 0) ;; " ▁▂▃▄▅▆▇█" (empty for 0) (sparkly-bars-user "▁▂▃▄▅▆▇█")) ;(9→8 steps) ;; shorter^
Besides that, you'll want to check the monospace font you're using.
Details about fonts
The monospace font that you choose can make a big difference on how your sparks look. Try them out.
Three issues are noteworthy here: space between lines, space between columns, and alignment at the base.
- Visual spacing between the lines of multiline sparks can vary from none to tiny to pronounced.
- When none, you can't quite see where one line ends and the other begins.
- When tiny, you see a very thin horizontal line of discontinuity separating lines.
- When pronounced, you see a sizable gap between the lines.
- When none, you can't quite see where one line ends and the other begins.
- A division between columns might also be noticeable in some fonts.
Finally, not every font aligns the low line with the base of the blocks, with the result that this:
▄ ▂ █ ___█▁_█_▂_▇_██▄_▆__might not show up with a straight line at the base from beginning to end.
Multiline sparks look better when:
- the space between lines is none or tiny (gaps look bad);
- the space between columns is none; and
- the low line (default glyph to represent zero) perfectly aligns with the base of blocks.
I sampled a few popular monospace fonts to see how they looked. In particular:
- The only one that perfectly matched these three requirements was the
DejaVu Sans Mono. Liberation MonoandNoto Monohad no spacing issues, but base alignment is imperfect.Noto Sans Monohad the worst result: it lacks base alignment, and the gap between lines is huge.
If your only issue is base alignment, and you don't want to change fonts, you can replace the
default glyph for zero (low line: "_") by customizing sparkly-bars-user.
Summary of callables
Here's an overview of this package's callables:
| Function | Summary |
|---|---|
| sparkly-vi | Insert vertical sparks from LIST at line below point. |
| sparkly-vs | Create vertical multiline sparks string from LIST. |
| sparkly-see-readme | Open sparkly's README.org file. |
| sparkly-see-news | See the News in sparkly's README.org file. |
They're described in more detail below.
Functions
Vertical multiline sparks
Given a list, create its corresponding vertical multiline sparks.
- sparkly-vi inserts the string
- sparkly-vs returns the string
sparkly-vi (list &optional key-1 value-1 key-2 value-2 ...)
Insert vertical sparks from LIST at line below point.
Return nil.
Optional key–value ARGS may be passed. See sparkly-vs for KEYS.
If aggressive-indent-mode is on, turn it off while inserting.
Autodetect where point is before inserting, and open lines
accordingly.
sparkly-vs (list &optional key-1 value-1 key-2 value-2 ...)
Create vertical multiline sparks string from LIST.
All elements of LIST must be numbers. Negative ones will be
flattened to zero.
Optional key–value ARGS may be passed.
The following KEYS are available:
| KEY | Explanation | Default |
|---|---|---|
:fac |
Stretching factor | 0.125 |
:lns |
Number of lines | nil |
:fit |
Whether to round up | nil |
:lim |
Maximum number of lines | nil |
:wrp |
Wrap spark at this width | nil (∞) |
:wsp |
Empty lines between wrapped sparks | nil (0) |
:bar |
String to represent spark steps | _▁▂▃▄▅▆▇█ |
Constraints:
:facand:lnsare mutually exclusive- values of
:fac,:lns, and:limmust be positive numbers - values of
:wrpandwspmust be non-negative integers - value of
:fitmust be a boolean
Details:
:facis a stretching factor to apply to all numbers of the list.:lnsis the exact number of lines to be occupied by the maximum
number of the list.:fitis whether to round up:lnsto the next integer.:limis the maximum number of lines allowed;
this is particularly useful if the input is variable and you want
to impose a ceiling on how many lines it might end up displaying.:wrpis the maximum width of the spark block; if 0, or nil, or
larger than the spark width, there won't be wrapping.:wspis the number of empty lines to add between each wrapped spark.:baris a string to represent spark steps.
Here's how these values are parsed from input:
- First, if
:lnsis nil, then:
- If
:facis nil, set it to ⅛ (= .125). - Set
:lnsas (* max:fac), where max is the LIST's maximum.
- If
- Then, if
:fitis t, round up:lnsto the next integer. - Then, if
:limis non-nil, set:lnsto the minimum value between
:limand:lns. - Finally, set
:facto (/:lnsmax 1.0)
This value of :fac is then applied to all the numbers.
Sparks will be represented by the first non-nil value in this list:
(sparkly-bars bar sparkly-bars-user sparkly-bars-preset)
The last has value "_▁▂▃▄▅▆▇█" and the others start as nil. So:
- Customizable
sparkly-bars-usercan be set to override the preset. - Passing
:baras sparkly argument will override both. - And let-binding
sparkly-barswill override all the three.
One advantage of this last one is that it can be wrapped around several
sparkly expressions. This is particularly useful when modifying it for
sparkly-stats-report (from sparkly-stats package), which see.
;;;; The 4 fills half of the line: 4 = ½*8 (sparkly-vs '(2 3 4)) => "▂▃▄" ;;;; Stretched so that 4 reaches the top of the line (sparkly-vs '(2 3 4) :fit t) => "▄▆█" ;;;; Same as above, because automatically numlines=1 (sparkly-vs '(2 3 4) :lns 1) => "▄▆█" ;;;; Stretched to fill three lines (sparkly-vs '(2 3 4) :lns 3) => "\ ▂█ ▄██ ███" ;;;; Stretched to max-value-of-seq lines (sparkly-vs '(2 3 4) :fac 1) => "\ █ ██ ███ ███" ;;;; The 9s automatically span to a second line (sparkly-vs '(3 1 4 1 5 9 2 6 5 3 5 8 9 7 9 3 2 3 8 4 6 2 6 4 3 3 8 3 2 7 9 5 0 2 8 8 4 1 9 7 1 6 9 3 9 9 3 7 5 1 0 5 8 2 0 9 7 4 9 4 4 5 9 2 3 0 7 8 1 6 4 0 6 2 8 6)) => "\ ▁ ▁ ▁ ▁ ▁ ▁ ▁▁ ▁ ▁ ▁ ▃▁▄▁▅█▂▆▅▃▅██▇█▃▂▃█▄▆▂▆▄▃▃█▃▂▇█▅_▂██▄▁█▇▁▆█▃██▃▇▅▁_▅█▂_█▇▄█▄▄▅█▂▃_▇█▁▆▄_▆▂█▆" ;;;; Here it automatically uses 13 lines: 97 = (12+⅛)*8 (sparkly-vs '(3 14 15 92 65 35 89 79 32 38 46 26 43 38 32 79 50 28 84 19 71 69 39 93 75 10 58 20 97 49 44 59 23 07 81 64 06)) ;;;; ⮯ => "\ ▁ ▄ ▁ ▅ █ █ █ ▄ █ █ ▁ █ █▇ ▇ █ █▃ █ █ █▁ ██ █ █ ▇▅ ██ █ █ ██ ██ █ █ ██ ██ ▂ █ ▃ ██ ██ ██ █▂ █ ██ ██ █ █▁ █ ██ ██ ██ ▆ ▃ ██ █ ██ ██ █ ██▄█ ██ ██▃██ ▆█ █▆ ██ █ ██▇██ █ ████ ██ ████████▂█████▄█ █████ █ ████ ██ ████████████████▃█████ █▄████▇ ██ ▆▇██████████████████████▂███████ ██ ▃████████████████████████████████▇██▆" ;;;; We can stretch it so that 97 reaches the top of the 13th line (sparkly-vs '(3 14 15 92 65 35 89 79 32 38 46 26 43 38 32 79 50 28 84 19 71 69 39 93 75 10 58 20 97 49 44 59 23 07 81 64 06) :fit t) ;;;; ⮯ => "\ ▃ ▄ █ █ ▇ ▂ █ █ █ █▅ ▅ █ █ █ ▇ █ ██ █ █ ▄▂ ██ █ █ █▆ ██ █ █ ██ ██ █ █▅ ██ ██ █ █ ██ ██ ▆ █ ▇ ██ ██ ██ ▁ █▆ █ ██ ██ █ █▅ █ ██ ██ ██ ▁█ ▆▁ ██ █ ██▂██ █ ██▇█ ██ ██▆██▂██ ██▂██ █ █████ █ ████ ██ ████████▄█████▆█ █████ █ ████▁ ██ ████████████████▄█████ █▅█████ ██ ▇███████████████████████▃███████ ██ ▃███████████████████████████████████▆" ;;;; Here we fit the max value (97) to exactly four lines (sparkly-vs '(3 14 15 92 65 35 89 79 32 38 46 26 43 38 32 79 50 28 84 19 71 69 39 93 75 10 58 20 97 49 44 59 23 07 81 64 06) :lns 4) ;;;; ⮯ => "\ ▆ ▅▂ ▂ ▄ ▇▁ █ ▃ █▅ ██ █ █ ▇▇ ██ ▃ █ ▃ █▅ ██▄██▃▅▇▁▆▅▃██▁█ ██▅██ █ ██▇█ ██ ▁▅▅████████████████▆█████▃█▇█████▂██▂" ;;;; Here we wrap the previous spark at a maximum width of 10 (sparkly-vs '(3 14 15 92 65 35 89 79 32 38 46 26 43 38 32 79 50 28 84 19 71 69 39 93 75 10 58 20 97 49 44 59 23 07 81 64 06) :lns 4 :wrp 10) => "\ ▆ ▅▂ █▅ ██ ██▄██▃▅ ▁▅▅███████ ▂ ▄ █ █ ▇▁▆▅▃██▁█ █████████▆ ▇▁ █ ▇▇ ██ ▃ █ ██▅██ █ ██ █████▃█▇██ ▃ ▃ █▅ ▇█ ██ ███▂██▂" ;;;; Same, but wrapping at 20 and adding two lines between the pieces (sparkly-vs '(3 14 15 92 65 35 89 79 32 38 46 26 43 38 32 79 50 28 84 19 71 69 39 93 75 10 58 20 97 49 44 59 23 07 81 64 06) :lns 4 :wrp 20 :wsp 2) => "\ ▆ ▅▂ ▂ ▄ █▅ ██ █ █ ██▄██▃▅▇▁▆▅▃██▁█ ▁▅▅████████████████▆ ▇▁ █ ▃ ▇▇ ██ ▃ █ ▃ █▅ ██▅██ █ ██▇█ ██ █████▃█▇█████▂██▂" ;;;; And here we fit it to a single line (sparkly-vs '(3 14 15 92 65 35 89 79 32 38 46 26 43 38 32 79 50 28 84 19 71 69 39 93 75 10 58 20 97 49 44 59 23 07 81 64 06) :lns 1) ;;;; ⮯ => "\ _▁▁█▅▃▇▇▃▃▄▂▄▃▃▇▄▂▇▂▆▆▃█▆▁▅▂█▄▄▅▂▁▇▅_" ;;;; You can have "natural" sizes with :fac 1 (let* ((π float-pi) ; 3.141592653589793 (e float-e) ; 2.718281828459045 (m (* π e)) ; 8.539734222673566 (s (+ π e)) ; 5.859874482048838 (q (/ π e)) ; 1.155727349790922 (d (- π e)) ; 0.423310825130748 (l (list π e 0 m s 0 q d 0 4 2 0))) (sparkly-vs l :fac 1)) => "\ ▄ █ █ █▇ ██ ▁ ██ █ █▆ ██ █ ██ ██ ▁ ██ ██_██_█▃_██_" ;;;; Wrapping removes any empty lines at the top of pieces, ;;;; which makes the output tight. For example: ;;;;; Here, the edges have many empty lines at their top (sparkly-vs '(10 15 18 4 19 8 27 45 1 4 1 4)) => "\ ▅ █ ▃█ ▂ ▃ ██ ▂▇█ █ ██ ███▄████▁▄▁▄" ;;;;; But when wrapping, each piece is tightly packed (sparkly-vs '(10 15 18 4 19 8 27 45 1 4 1 4) :wrp 4) => "\ ▂ ▂▇█ ███▄ ▅ █ ▃█ ▃ ██ █ ██ ████ ▁▄▁▄" ;;;;; Yet you can add empty lines between pieces, if you want (sparkly-vs '(10 15 18 4 19 8 27 45 1 4 1 4) :wrp 4 :wsp 1) => "\ ▂ ▂▇█ ███▄ ▅ █ ▃█ ▃ ██ █ ██ ████ ▁▄▁▄" ;;;; Additional simple examples ;;;;; Naturally uses 2.5 lines (sparkly-vs '(5 10 20)) => "\ ▄ ▂█ ▅██" ;;;;; Still 2.5 lines (sparkly-vs '(5 10 20) :lim 3) => "\ ▄ ▂█ ▅██" ;;;;; Stretched to exactly 3 (sparkly-vs '(5 10 20) :fit t) => "\ █ ▄█ ▆██" (sparkly-vs '(5 10 20) :lns 3) => "\ █ ▄█ ▆██" ;;;;; Compressed to exactly 2 (sparkly-vs '(5 10 20) :lim 2) => "\ █ ▄██" (sparkly-vs '(5 10 20) :lns 2) => "\ █ ▄██" ;;;;; Compressed to exactly 1 (sparkly-vs '(5 10 20) :lns 1) => "▂▄█" ;;;; Special cases ;;;;; Arg :lns need not be an integer (sparkly-vs '(1 6 2) :lns 1) => "▁█▃" (sparkly-vs '(1 6 2) :lns .6) => "▁▅▂" (sparkly-vs '(1 6 2) :lns .001) => "___" (sparkly-vs '(1 6 2) :lns (sqrt 2)) => "\ ▃ ▂█▄" ;<-- max value 6 occupies ~√2 lines (sparkly-vs '(1 6 2) :lns float-pi) => "\ ▁ █ █ ▄██" ;<-- max value 6 occupies ~π lines ;;;;; Empty list returns empty string (sparkly-vs '()) => "" (sparkly-vs '() :lns 2) => "" (sparkly-vs '() :lim 2) => "" (sparkly-vs '() :fit t) => "" ;;;;; Negative numbers are flattened to zero... (sparkly-vs '(-5 2 4)) => "_▂▄" (sparkly-vs '( 2 -5 4)) => "▂_▄" (sparkly-vs '( 2 4 -5)) => "▂▄_" ;;;;; ...and zero shows as low line (sparkly-vs '( 0 2 4)) => "_▂▄" (sparkly-vs '( 2 0 4)) => "▂_▄" (sparkly-vs '( 2 4 0)) => "▂▄_" (sparkly-vs '( 0 0 0)) => "___" ;;;;; But you can override that — the whole bars, actually ;;;;;; by customizing sparkly-bars-user (overrides _▁▂▃▄▅▆▇█) ;; (no examples) sparkly-bars-preset^ ;;;;;; by passing a value to :bar (overrides both) ;;;;;;; Default (sparkly-vs '(0 4 0 0 2 7 0)) => "_▄__▂▇_" ;;;;;;; Replace the zero: from "_" to " " (sparkly-vs '(0 4 0 0 2 7 0) :bar " ▁▂▃▄▅▆▇█") => " ▄ ▂▇" ;;;;;;; Replace the zero: from "_" to "." (sparkly-vs '(0 4 0 0 2 7 0) :bar ".▁▂▃▄▅▆▇█") => ".▄..▂▇." ;;;;;;; Replace the zero: from "_" to "▁" by removing the first (sparkly-vs '(0 4 0 0 2 7 0) :bar "▁▂▃▄▅▆▇█") => "▁▅▁▁▃█▁" ;;;;;;; Build it with 5 instead of 9 (sparkly-vs '(0 4 0 0 2 7 0) :bar "_▂▄▆█") => "\ ▆ _█__▄█_" (sparkly-vs '(0 4 0 0 2 7 0) :bar " ░▒▓█") => "\ ▓ █ ▒█" (sparkly-vs '(0 4 0 0 2 7 0) :bar "○◔◑◕●") => "\ ◕ ○●○○◑●○" ;;;;;; by let-binding sparkly-bars (overrides all) (let ((sparkly-bars " ▁▂▃▄▅▆▇█")) (sparkly-vs '(0 4 0 0 2 7 0))) => " ▄ ▂▇" (let ((sparkly-bars ".▁▂▃▄▅▆▇█")) (sparkly-vs '(0 4 0 0 2 7 0) :bar "_▂▄▆█")) => ".▄..▂▇." ;;;; Errors (sparkly-vs [5 20] :lns 4 :fac 1) !!> error ; not a list (sparkly-vs '(5 20) :lns 4 :fac 1) !!> error ; mutually exclusive (sparkly-vs '(5 20) :fit 2) !!> error ; not a boolean (sparkly-vs '(5 20) :lns 0) !!> error ; ≤ 0 (sparkly-vs '(5 20) :fac 0) !!> error (sparkly-vs '(5 20) :lim 0) !!> error (sparkly-vs '(5 20) :lns -1) !!> error (sparkly-vs '(5 20) :fac -1) !!> error (sparkly-vs '(5 20) :lim -1) !!> error
Commands
See README
Commands to open sparkly's README.org. Optionally, find things in it.
sparkly-see-readme (&optional heading narrow)
Open sparkly's README.org file.
Search for the file in sparkly.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.3.2
Enhancements
Wrapped pieces are now tightly packed
Up to here, when wrapping a multiline spark, every piece would take the total number of lines.
For example, consider this 12-column specimen:
▅ ; 6
█ ; 5
▃█ ; 4
▂ ▃ ██ ; 3
▂▇█ █ ██ ; 2
███▄████▁▄▁▄ ; 1
If we wrapped it in three pieces of four, each piece would use six lines.
This would take 3×6 = 18 lines of your screen.
Now the three pieces wrap tightly, taking only 3+6+1 = 10 lines:
(sparkly-vi '(10 15 18 4 19 8 27 45 1 4 1 4) :wrp 4) ▂ ; 10 ▂▇█ ; 9 ███▄ ; 8 ▅ ; 7 █ ; 6 ▃█ ; 5 ▃ ██ ; 4 █ ██ ; 3 ████ ; 2 ▁▄▁▄ ; 1
This is unrelated with the number of empty lines between pieces,
which still defaults to 0 and still can be changed with the :wsp key.
0.3.1
Sparkly News
This release brings improved documentation about customizing Sparkly and details about fonts.
0.3.0
Sparkly News
This release brings new features. You can now:
- wrap sparks to the desired width, and
- change the glyphs that compose the sparks
If you have Democratize, run M-x democratize-library to update the available examples.
New features
New keys for sparkly-vs
With the new key :wrp, you can now wrap long multiline sparks to a specified width.
And with :wsp you control the number of empty lines to add between each wrapped spark.
Wrapping is particularly useful for visualizing long sparks that don't fit your screen.
There's also :bar now, with which you can pass a string different than "_▁▂▃▄▅▆▇█" to represent your sparks.
New variable sparkly-bars
Not to be changed directly.
Rather, it can be let-bound around spark-generating code when modification is desired.
See sparkly-vs docstring for more information, as well as the new examples using it.
See also
Other packages
You can integrate sparkly-vs's examples into Helpful or Help buffers with another package of mine called Democratize, which can also integrate examples from xht, dash, s, f, and native Emacs libraries.
And with Sparkly Stats you can create statistics and sparks from daily data.
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.el
Structure
;;; sparkly.el --- Create multiline sparks ▁▂▆▄▃▇▅ -*- lexical-binding: t -*- ;;; Commentary: ;;;; For all the details, please do see the README ;;; Code: ;;;; Libraries ;;;; Package metadata ;;;; Customizable variables ;;;; Other variables ;;;; Functions ;;;;; Description macro ;;;;; Vertical multiline sparks ;;;; Commands ;;;;; See README ;;;; Wrapping up ;;; sparkly.el ends here
Contents
;;; sparkly.el --- Create multiline sparks ▁▂▆▄▃▇▅ -*- 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.3.2 ;; Package-Requires: ((emacs "25.1") (dash "2.18") (s "1.4")) ;;--------------------------------------------------------------------------- ;; 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 you can create multiline sparks: ;; ;; ▆ ▅▂ ▂ ▄ ▇▁ █ ▃ ;; █▅ ██ █ █ ▇▇ ██ ▃ █ ▃ █▅ ;; ██▄██▃▅▇▁▆▅▃██▁█ ██▅██ █ ██▇█ ██ ;; ▁▅▅████████████████▆█████▃█▇█████▂██▂ ;; ;;;; 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-see-readme ;; ;; or read it online: ;; <https://flandrew.srht.site/listful/sw-emacs-sparkly.html> ;; ;; ¹ or the key that ‘eval-last-sexp’ is bound to, if not C-x C-e. ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Code: ;;;; Libraries (require 's) (require 'dash) ; ‘-iota’, therefore 2.18+ (require 'lisp-mnt) ; ‘lm-summary’, ‘lm-homepage’, ‘lm-version’, ‘lm-header’ ;;;; Package metadata (defvar sparkly--name "Sparkly") (defvar sparkly--dot-el (format "%s.el" (file-name-sans-extension (eval-and-compile (or load-file-name buffer-file-name))))) (defvar sparkly--readme-org (expand-file-name "README.org" (file-name-directory sparkly--dot-el))) (defvar sparkly--summary (lm-summary sparkly--dot-el)) (defvar sparkly--homepage (lm-homepage sparkly--dot-el)) (defvar sparkly--version (lm-with-file sparkly--dot-el (or (lm-header "package-version") (lm-version)))) ;;;; Customizable variables (defgroup sparkly nil (format "%s." sparkly--summary) :group 'extensions :link '(emacs-library-link :tag "Lisp file" "sparkly.el") :link `(file-link :tag "README.org" ,sparkly--readme-org) :link `(url-link :tag "Homepage" ,sparkly--homepage)) (defcustom sparkly-bars-user nil "User-defined default string for spark steps. If non-nil, it'll always override ‘sparkly-bars-preset’." :package-version '(sparkly "0.3.0") :type 'string) ;;;; Other variables (defvar sparkly-bars-preset "_▁▂▃▄▅▆▇█" "Preset default string to represent spark steps.") (defvar sparkly-bars nil "Actual string that will be used to represent spark steps. This variable should not be directly changed. Rather, it should be let-bound around spark-generating code when modification is desired.") ;;;; Functions ;;;;; Description macro (defmacro sparkly--describe (str) "Describe with string STR the contents under this heading. Think of it as a docstring for the headings of your elisp files. For the full docstring, look for ‘orgreadme-fy-describe’ in the package ‘orgreadme-fy’." (declare (indent 0)) (unless (stringp str) (user-error "‘sparkly--describe’ must receive a string"))) ;;;;; Vertical multiline sparks (sparkly--describe "Given a list, create its corresponding vertical multiline sparks. - ‘sparkly-vi’ inserts the string - ‘sparkly-vs’ returns the string") (defun sparkly-vi (list &rest args) "Insert vertical sparks from LIST at line below point. Return nil. Optional key–value ARGS may be passed. See ‘sparkly-vs’ for KEYS. If ‘aggressive-indent-mode’ is on, turn it off while inserting. Autodetect where point is before inserting, and open lines accordingly. \(fn LIST &optional KEY-1 VALUE-1 KEY-2 VALUE-2 ...)" (sparkly--smart-insert (apply #'sparkly-vs list args))) (defun sparkly-vs (list &rest args) "Create vertical multiline sparks string from LIST. All elements of LIST must be numbers. Negative ones will be flattened to zero. Optional key–value ARGS may be passed. The following KEYS are available: | KEY | Explanation | Default | |--------+------------------------------------+-----------| | `:fac' | Stretching factor | 0.125 | | `:lns' | Number of lines | nil | | `:fit' | Whether to round up | nil | | `:lim' | Maximum number of lines | nil | |--------+------------------------------------+-----------| | `:wrp' | Wrap spark at this width | nil (∞) | | `:wsp' | Empty lines between wrapped sparks | nil (0) | | `:bar' | String to represent spark steps | _▁▂▃▄▅▆▇█ | Constraints: - `:fac' and `:lns' are mutually exclusive - values of `:fac', `:lns', and `:lim' must be positive numbers - values of `:wrp' and `wsp' must be non-negative integers - value of `:fit' must be a boolean Details: - `:fac' is a stretching factor to apply to all numbers of the list. - `:lns' is the exact number of lines to be occupied by the maximum number of the list. - `:fit' is whether to round up `:lns' to the next integer. - `:lim' is the maximum number of lines allowed; this is particularly useful if the input is variable and you want to impose a ceiling on how many lines it might end up displaying. - `:wrp' is the maximum width of the spark block; if 0, or nil, or larger than the spark width, there won't be wrapping. - `:wsp' is the number of empty lines to add between each wrapped spark. - `:bar' is a string to represent spark steps. Here's how these values are parsed from input: - First, if `:lns' is nil, then: - If `:fac' is nil, set it to ⅛ (= .125). - Set `:lns' as (* max `:fac'), where max is the LIST's maximum. - Then, if `:fit' is t, round up `:lns' to the next integer. - Then, if `:lim' is non-nil, set `:lns' to the minimum value between `:lim' and `:lns'. - Finally, set `:fac' to (/ `:lns' max 1.0) This value of `:fac' is then applied to all the numbers. Sparks will be represented by the first non-nil value in this list: (sparkly-bars bar sparkly-bars-user sparkly-bars-preset) The last has value \"_▁▂▃▄▅▆▇█\" and the others start as nil. So: - Customizable ‘sparkly-bars-user’ can be set to override the preset. - Passing `:bar' as sparkly argument will override both. - And let-binding ‘sparkly-bars’ will override all the three. One advantage of this last one is that it can be wrapped around several sparkly expressions. This is particularly useful when modifying it for ‘sparkly-stats-report’ (from sparkly-stats package), which see. \(fn LIST &optional KEY-1 VALUE-1 KEY-2 VALUE-2 ...)" (if (null list) "" (-let [(lns fac wrp wsp bar) (apply #'sparkly--parse list args)] (let* ((barwid (1- (length bar))) ; 1- number of glyphs (barzer (substring bar 0 1)) ; first glyph: the zero (baremp (concat " " (substring bar 1))) ; first glyph made " " (num2ls (lambda (num) (let ((norm (round (* barwid fac num)))) (--map (number-to-string (let ((Δ (- norm (* barwid it)))) (if (>= Δ barwid) barwid (if (<= Δ 0) 0 Δ)))) (-iota (ceiling lns)))))) (sparkl (-map num2ls list)) (sparkn (apply #'-zip-lists sparkl)) (sparka (-zip-pair (-map #'number-to-string (-iota (1+ barwid))) (-map #'string baremp))) (sparks (--map (s-replace-all sparka (s-join "" it)) sparkn)) (linlen (length (car sparks)))) ;; The bottom line (and only it) will have the 0s replaced (setcar sparks (s-replace " " barzer (car sparks))) (setq sparks (nreverse sparks)) ;; We can now possibly wrap, remove trailing spaces, and finish (when (or (null wrp) (= wrp 0) (> wrp linlen)) (setq wrp linlen)) (s-trim-right (with-output-to-string (dolist (sub (sparkly--partition-steps linlen wrp)) (dolist (lin sparks) (--> (s-trim-right (apply #'substring lin sub)) (unless (s-blank? it) (princ it) (terpri)))) (dotimes (_i (or wsp 0)) (terpri))))))))) (defun sparkly--partition-steps (len wrp) "Create partition steps for string length LEN and wrapping at WRP." (let ((max (* wrp (/ (1- len) wrp)))) (-partition-all-in-steps 2 1 (number-sequence 0 max wrp)))) (defun sparkly--parse (list &rest args) "Parse LIST. Return (LNS FAC WRP WSP BAR). Optional key–value ARGS may be passed. See ‘sparkly-vs’ for KEYS. \n(fn LIST &optional KEY-1 VALUE-1 KEY-2 VALUE-2 ...)" (-let [(&plist :fac :fit :lim :lns :wrp :wsp :bar) args] ;; Check for errors (or (listp list) (error "%s must be a list" list)) (or (booleanp fit) (error "%s must be a boolean" :fit)) (and lns fac (error "%s and %s are mutually exclusive params" :lns :fac)) (--each `((:lns ,lns) (:fac ,fac) (:lim ,lim)) (-let [(k v) it] (or (null v) (and (numberp v) (> v 0)) (error "%s, if given, must be a positive number" k)))) (--each `((:wrp ,wrp) (:wsp ,wsp)) (-let [(k v) it] (or (null v) (and (integerp v) (>= v 0)) (error "%s, if given, must be a non-negative integer" k)))) (or (null bar) (stringp bar) (error "%s, if given, must be a string" :bar)) ;; Produce output (let ((max (-max list))) (setq bar (or sparkly-bars bar sparkly-bars-user sparkly-bars-preset)) (or fac (setq fac (/ 1.0 (1- (length bar))))) (or lns (setq lns (if (= max 0) 1 (* max fac)))) (when fit (setq lns (ceiling lns))) (when lim (setq lns (min lim lns))) (when (> max 0) (setq fac (/ lns max 1.0))) (list lns fac wrp wsp bar)))) (defun sparkly--smart-insert (string) "Insert STRING into line after point without indenting." (save-excursion (atomic-change-group (unless (bolp) (forward-line)) (unless (eolp) (open-line 1)) (if (and (fboundp 'aggressive-indent-mode) (bound-and-true-p aggressive-indent-mode)) (prog2 (aggressive-indent-mode -1) (insert string) (aggressive-indent-mode 1)) (insert string))))) ;;;; Commands ;;;;; See README (sparkly--describe "Commands to open sparkly's README.org. Optionally, find things in it.") ;;;###autoload (defun sparkly-see-readme (&optional heading narrow) "Open sparkly's README.org file. Search for the file in sparkly.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--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--goto-org-heading heading narrow)) (progress-reporter-done pr)) (message "Couldn't find %s's README.org" sparkly--name)))) ;;;###autoload (defun sparkly-see-news () "See the News in sparkly's README.org file." (interactive) (sparkly-see-readme "News" 'narrow) (sparkly--display-org-subtree)) (defun sparkly--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--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) ;; Local Variables: ;; coding: utf-8 ;; indent-tabs-mode: nil ;; sentence-end-double-space: nil ;; outline-regexp: ";;;;* " ;; End: ;;; sparkly.el ends here
📆 2025-01-09