2023-08-05 Plotting ASCII art charts from Org mode tables

Three weeks ago I wrote about simple ASCII art charts in Emacs. Now I’m going to use Org mode tables as the source of the data for them. Since my use case is plotting my weight against time, this will get a bit more complicated because the values of the independent variable are dates.

Let’s recall that my table looks like this.

#+name: weight-data
| Date+time              | Weight |   MA |
|------------------------+--------+------|
| [2023-04-30 Sun 19:23] |   66.1 |    0 |
| [2023-05-01 Mon 05:13] |   66.2 |    0 |
| [2023-05-04 Thu 04:56] |   66.0 |    0 |
| [2023-05-05 Fri 05:08] |   65.8 |    0 |
| [2023-05-06 Sat 19:23] |   66.0 |    0 |
| [2023-05-07 Sun 05:13] |   65.9 |    0 |
| [2023-05-10 Wed 04:56] |   65.6 | 65.9 |
| [2023-05-12 Fri 05:15] |   65.7 | 65.9 |
| [2023-05-13 Sat 05:07] |   65.8 | 65.8 |
#+TBLFM: $3=vmean(@-6$-1..@0$-1);%.1f::@2$3=0::@3$3=0::@4$3=0::@5$3=0::@6$3=0::@7$3=0

Let’s start with getting the data from a table. I know about two ways of doing this. One is the org-table-get-remote-range function. You give it the name of the table (that’s why I added the first line with the name) and the (absolute) range of cells and get a list of strings containing those cells’ data. It is not extremely convenient for our purpose, though – it returns a flat list even if the range is a rectangle of both width and height greater than one. Still, it is fairly easy to use it to get, say, the first three columns of the last two rows of the table:

(mapcar
 #'substring-no-properties
 (org-table-get-remote-range "weight-data" "@>>$1..@>$3"))

(The substring-no-properties is here so that I can yank this form into M-: and see the actual string contents, without all the distracting information about Org text properties.)

Of course, I need the last sixty rows, and putting sixty > signs there by hand is not the best idea. But I can say

(mapcar
 #'substring-no-properties
 (org-table-get-remote-range
  "weight-data"
  (format "@%s$1..@>$3" (make-string 60 ?>))))

There is, however, another, more Org-ish way to do this. We can use an Elisp source block and feed it the data from a table. For example, pressing C-c C-c on this:

#+name: weight-plot
#+begin_src elisp :var data=weight-data :results code
  (cl-subseq data -2)
#+end_src

will result in this:

(("[2023-05-12 Fri 05:15]" 65.7 65.9)
 ("[2023-05-13 Sat 05:07]" 65.8 65.8))

(The :results code causes Org to show the output in a lispy way instead as an Org table.) In this example, (cl-subseq data -2) returns the subsequence of data from the penultimate element on, that is, two last elements. (Of course, I had to (require 'cl) for this to work.)

Now, let’s map over the, say, last 60 elements, disregard the second element of every one of them (to plot only the moving average), and translate the timestamp to the number of days between today and the day specified in the timestamp.

The trickiest thing here is to do time calculations (that is, compute how many days passed from a date, given as a string in Org mode timestamp format). There are three functions which are of help here: org-timestamp-from-string (which converts a string to an Org timestamp object), org-timestamp-to-time (which converts an Org timestamp object to Emacs time value) and time-to-days (which converts an Emacs time value to the number of days from 0001-01-01). Here is how I used Org source blocks to create a list suitable for my plotting functions:

#+begin_src elisp :var data=weight-data :results code
  (mapcar (lambda (row)
	    (let ((timestamp (car row))
		  (value (caddr row)))
	      (cons (- (time-to-days (org-timestamp-to-time (org-timestamp-from-string timestamp)))
		       (time-to-days (current-time)))
		    value)))
	  (cl-subseq data -2))
#+end_src

Now the only thing remaining is to feed it to scatter-plot.

#+begin_src elisp :var data=weight-data :wrap example
  (scatter-plot
   (mapcar (lambda (row)
	     (let ((timestamp (car row))
		   (value (caddr row)))
	       (cons (- (time-to-days (org-timestamp-to-time (org-timestamp-from-string timestamp)))
			(time-to-days (current-time)))
		     value)))
	   (cl-subseq data -60))
   '(:width 60 :height 24 :labelx-skip 10 :labely-skip 0.5 :char ?#))
#+end_src

Note that I used :wrap example (as suggested by Ihor Radchenko on the mailing list). The :results raw option didn’t work because Org interpreted the | signs as a table and botched the formatting. Also, I needed to change the character used to mark the points on the chart to # since Org prepended a comma to the asterisk to avoid treating that line as a headline.

And now I can see my weight data as a plot directly in Org mode!

CategoryEnglish, CategoryBlog, CategoryEmacs, CategoryOrgMode