Using the calendar-date-echo-text variable to help plot a heatmap on a year-long calendar in Emacs
| emacs
Building on Display a calendar heat map using Emacs Lisp,
I figured out how to use calendar-date-echo-text
to store
the date so that I can pick it up when plotting the heatmap:
;; This seems to be the only way we can hack the date in for now (setq calendar-date-echo-text '(apply #'format (list "%04d-%02d-%02d" year month day))) (defun my-calendar-heat-map-using-echo-text (&rest _) (when my-calendar-count-scaled (save-excursion (goto-char (point-min)) (while (not (eobp)) (let* ((help (get-text-property (point) 'help-echo)) (next-change (or (next-single-property-change (point) 'help-echo) (point-max))) (inhibit-read-only t) (count-scaled (and help (assoc-default help my-calendar-count-scaled)))) (when (and help (string-match "[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]" help) count-scaled) (put-text-property (point) (+ 2 (point)) 'face (intern (format "calendar-scale-%d" count-scaled)))) (goto-char next-change)))))) (advice-add #'calendar :after #'my-calendar-heat-map-using-echo-text) (advice-add #'calendar-redraw :after #'my-calendar-heat-map-using-echo-text) (advice-add #'year-calendar :after #'my-calendar-heat-map-using-echo-text) (add-hook 'calendar-move-hook #'my-calendar-heat-map-using-echo-text)
So now I don't need the advice around calendar-generate-month
, just
the code that sets up the faces, loads the values, and figures out the
data.
Previous source code (tweaked foreground colours)
(defface calendar-scale-1 '((((background light)) :foreground "black" :background "#eceff1") (((background dark)) :foreground "white" :background "#263238")) "") (defface calendar-scale-2 '((((background light)) :foreground "black" :background "#cfd8dc") (((background dark)) :foreground "white" :background "#37474f")) "") (defface calendar-scale-3 '((((background light)) :foreground "black" :background "#b0bec5") (((background dark)) :foreground "white" :background "#455a64")) "") (defface calendar-scale-4 '((((background light)) :foreground "black" :background "#90a4ae") (((background dark)) :foreground "white" :background "#546e7a")) "") (defface calendar-scale-5 '((((background light)) :foreground "black" :background "#78909c") (((background dark)) :foreground "white" :background "#607d8b")) "") (defface calendar-scale-6 '((((background light)) :foreground "white" :background "#607d8b") (((background dark)) :foreground "black" :background "#78909c")) "") (defface calendar-scale-7 '((((background light)) :foreground "white" :background "#546e7a") (((background dark)) :foreground "black" :background "#90a4ae")) "") (defface calendar-scale-8 '((((background light)) :foreground "white" :background "#455a64") (((background dark)) :foreground "black" :background "#b0bec5")) "") (defface calendar-scale-9 '((((background light)) :foreground "white" :background "#37474f") (((background dark)) :foreground "black" :background "#cfd8dc")) "") (defun my-count-calendar-entries (grouped-entries) (mapcar (lambda (entry) (cons (car entry) (length (cdr entry)))) grouped-entries)) (defface calendar-scale-10 '((((background light)) :foreground "white" :background "#263238") (((background dark)) :foreground "black" :background "#eceff1")) "") (defun my-scale-calendar-entries (grouped-entries &optional scale-max) (let* ((count (my-count-calendar-entries grouped-entries)) (count-max (apply #'max (mapcar (lambda (o) (if (car o) (cdr o) 0)) count)))) (mapcar (lambda (entry) (cons (car entry) (/ (* 1.0 (or scale-max 1.0) (cdr entry)) count-max))) count))) (defun my-scale-calendar-entries-logarithmically (grouped-entries &optional scale-max) (let* ((count (my-count-calendar-entries grouped-entries)) (count-max (apply #'max (mapcar (lambda (o) (if (car o) (cdr o) 0)) count)))) (mapcar (lambda (entry) (cons (car entry) (/ (* 1.0 (or scale-max 1.0) (log (cdr entry))) (log count-max)))) count))) (defvar my-calendar-count-scaled nil "Values to display.")
Now I can have it display the last year of data or so.
(defun my-calendar-visualize (values) (setq my-calendar-count-scaled values) (let* ((date (calendar-current-date)) (month (calendar-extract-month date)) (year (calendar-extract-year date))) (year-calendar month (1- year))))
The code to load the data stays the same.
Loading the data
(defun my-calendar-visualize-journal-entries () (interactive) (my-calendar-visualize (mapcar (lambda (o) (cons (car o) (ceiling (+ 1 (* 7.0 (cdr o)))))) (my-scale-calendar-entries (seq-group-by #'my-journal-date (cdr (pcsv-parse-file "~/Downloads/entries.csv"))))))) (defun my-calendar-visualize-sketches () (interactive) (let ((my-calendar-sketches (assoc-delete-all nil (seq-group-by (lambda (o) (when (string-match "^\\([0-9][0-9][0-9][0-9]\\)[-_]?\\([0-9][0-9]\\)[-_]?\\([0-9][0-9]\\)" o) (format "%s-%s-%s" (match-string 1 o) (match-string 2 o) (match-string 3 o)))) (append (directory-files "~/sync/sketches" nil "\\.\\(png\\|jpg\\)\\'") (directory-files "~/sync/private-sketches" nil "\\.\\(png\\|jpg\\)\\'")))))) (my-calendar-visualize (mapcar (lambda (o) (cons (car o) ;; many days have just 1 sketch, so I set the low end of the scale ;; to make them visible, and use a logarithmic scale for the rest (ceiling (+ 3 (* 7.0 (cdr o)))))) (my-scale-calendar-entries-logarithmically my-calendar-sketches))))) (defun my-calendar-visualize-tantrums () (interactive) (my-calendar-visualize (mapcar (lambda (o) (cons (car o) (ceiling (* 10.0 (cdr o))))) (my-scale-calendar-entries (seq-group-by #'my-journal-date (seq-filter (lambda (o) (string-match "tantrum\\|grump\\|angry\\|meltdown" (my-journal-note o))) (cdr (pcsv-parse-file "~/Downloads/entries.csv"))))))))
Here's the code from lawlist's StackOverflow answer that displays the Emacs calendar for a year:
Source code for showing an Emacs calendar year
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; ;;; ;;; Scroll a yearly calendar by month -- in a forwards or backwards direction. ;;; ;;; ;;; ;;; To try out this example, evaluate the entire code snippet and type: ;;; ;;; ;;; ;;; M-x year-calendar ;;; ;;; ;;; ;;; To scroll forward by month, type the key: > ;;; ;;; ;;; ;;; To scroll backward by month, type the key: < ;;; ;;; ;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (eval-after-load "calendar" '(progn (define-key calendar-mode-map "<" 'lawlist-scroll-year-calendar-backward) (define-key calendar-mode-map ">" 'lawlist-scroll-year-calendar-forward) )) (defmacro lawlist-calendar-for-loop (var from init to final do &rest body) "Execute a for loop. Evaluate BODY with VAR bound to successive integers from INIT to FINAL, inclusive. The standard macro `dotimes' is preferable in most cases." `(let ((,var (1- ,init))) (while (>= ,final (setq ,var (1+ ,var))) ,@body))) (defun year-calendar (&optional month year) "Generate a one (1) year calendar that can be scrolled by month in each direction. This is a modification of: http://homepage3.nifty.com/oatu/emacs/calendar.html See also: http://ivan.kanis.fr/caly.el" (interactive) (require 'calendar) (let* ((current-year (number-to-string (nth 5 (decode-time (current-time))))) (month (if month month (string-to-number (read-string "Please enter a month number (e.g., 1): " nil nil "1")))) (year (if year year (string-to-number (read-string "Please enter a year (e.g., 2014): " nil nil current-year))))) (switch-to-buffer (get-buffer-create calendar-buffer)) (when (not (eq major-mode 'calendar-mode)) (calendar-mode)) (setq displayed-month month) (setq displayed-year year) (setq buffer-read-only nil) (erase-buffer) ;; horizontal rows (lawlist-calendar-for-loop j from 0 to 3 do ;; vertical columns (lawlist-calendar-for-loop i from 0 to 2 do (calendar-generate-month ;; month (cond ((> (+ (* j 3) i month) 12) (- (+ (* j 3) i month) 12)) (t (+ (* j 3) i month))) ;; year (cond ((> (+ (* j 3) i month) 12) (+ year 1)) (t year)) ;; indentation / spacing between months (+ 5 (* 25 i)))) (goto-char (point-max)) (insert (make-string (- 10 (count-lines (point-min) (point-max))) ?\n)) (widen) (goto-char (point-max)) (narrow-to-region (point-max) (point-max))) (widen) (goto-char (point-min)) (setq buffer-read-only t))) (defun lawlist-scroll-year-calendar-forward (&optional arg event) "Scroll the yearly calendar by month in a forward direction." (interactive (list (prefix-numeric-value current-prefix-arg) last-nonmenu-event)) (unless arg (setq arg 1)) (save-selected-window (if (setq event (event-start event)) (select-window (posn-window event))) (unless (zerop arg) (let ((month displayed-month) (year displayed-year)) (calendar-increment-month month year arg) (year-calendar month year))) (goto-char (point-min)) (run-hooks 'calendar-move-hook))) (defun lawlist-scroll-year-calendar-backward (&optional arg event) "Scroll the yearly calendar by month in a backward direction." (interactive (list (prefix-numeric-value current-prefix-arg) last-nonmenu-event)) (lawlist-scroll-year-calendar-forward (- (or arg 1)) event))
It might be fun to scroll by year:
(defun my-scroll-year-calendar-forward-year (&optional arg event) "Scroll the yearly calendar by year in a forward direction." (interactive (list (prefix-numeric-value current-prefix-arg) last-nonmenu-event)) (unless arg (setq arg 1)) (save-selected-window (if (setq event (event-start event)) (select-window (posn-window event))) (unless (zerop arg) (setq displayed-year (+ (or arg 1) displayed-year)) (year-calendar displayed-month displayed-year)) (goto-char (point-min)) (run-hooks 'calendar-move-hook))) (defun my-scroll-year-calendar-backward-year (&optional arg event) "Scroll the yearly calendar by month in a backward direction." (interactive (list (prefix-numeric-value current-prefix-arg) last-nonmenu-event)) (my-scroll-year-calendar-forward-year (- (or arg 1)) event)) (eval-after-load "calendar" '(progn (define-key calendar-mode-map "{" 'my-scroll-year-calendar-backward-year) (define-key calendar-mode-map "}" 'my-scroll-year-calendar-forward-year)))
I used M-x gif-screencast
to make the animated GIF. Yay Emacs!
You can comment with Disqus or you can e-mail me at sacha@sachachua.com.