This repository has been archived on 2022-05-13. You can view files and clone it, but cannot push or open issues or pull requests.
chronometrist/elisp/chronometrist-spark.org

12 KiB

chronometrist-spark

Library headers and commentary

;;; chronometrist-spark.el --- Show sparklines in Chronometrist -*- lexical-binding: t; -*-

;; Author: contrapunctus <xmpp:contrapunctus@jabjab.de>
;; Maintainer: contrapunctus <xmpp:contrapunctus@jabjab.de>
;; Keywords: calendar
;; Homepage: https://tildegit.org/contrapunctus/chronometrist
;; Package-Requires: ((emacs "24.3") (chronometrist "0.7.0") (spark "0.1"))
;; Version: 0.1.0

;; This is free and unencumbered software released into the public domain.
;;
;; Anyone is free to copy, modify, publish, use, compile, sell, or
;; distribute this software, either in source code form or as a compiled
;; binary, for any purpose, commercial or non-commercial, and by any
;; means.
;;
;; For more information, please refer to <https://unlicense.org>

"Commentary" is displayed when the user clicks on the package's entry in M-x list-packages.

;;; Commentary:
;;
;; This package adds a column to Chronometrist displaying sparklines for each task.

Dependencies

;;; Code:
(require 'chronometrist)
(require 'spark)

Code

custom group   custom group

(defgroup chronometrist-spark nil
  "Show sparklines in `chronometrist'."
  :group 'applications)

length   custom variable

(defcustom chronometrist-spark-length 7
  "Length of each sparkline in number of days."
  :type 'integer)

show-range   custom variable

(defcustom chronometrist-spark-show-range t
  "If non-nil, display range of each sparkline."
  :type 'boolean)

range   function

(defun chronometrist-spark-range (durations)
  "Return range for DURATIONS as a string.
DURATIONS must be a list of integer seconds."
  (let* ((duration-minutes  (--map (/ it 60) durations))
         (durations-nonzero (seq-remove #'zerop duration-minutes))
         (length            (length durations-nonzero)))
    (cond ((not durations-nonzero) "")
          ((> length 1)
           (format "(%sm~%sm)" (apply #'min durations-nonzero)
                   (apply #'max duration-minutes)))
          ((= 1 length)
           ;; This task only had activity on one day in the given
           ;; range of days - these durations, then, cannot really
           ;; have a minimum and maximum range.
           (format "(%sm)" (apply #'max duration-minutes))))))

tests

(ert-deftest chronometrist-spark-range ()
  (should (equal (chronometrist-spark-range '(0 0 0))    ""))
  (should (equal (chronometrist-spark-range '(0 1 2))    ""))
  (should (equal (chronometrist-spark-range '(60 0 0))   "(1m)"))
  (should (equal (chronometrist-spark-range '(60 0 120)) "(1m~2m)")))

TODO row-transformer   function

if larger than 7 add space after (% length 7)th element then add space after every 7 elements

  • if task has no time tracked for it - ""
  • don't display 0 as the minimum time tracked
  • if task has only one day of time tracked - "(40m)"
(defun chronometrist-spark-row-transformer (row)
  "Add a sparkline cell to ROW.
Used to add a sparkline column to `chronometrist-rows'.

ROW must be a valid element of the list specified by
`tabulated-list-entries'."
  (-let* (((task vector) row)
          (sparkline
           (cl-loop with today = (ts-now)
             with duration with active-p
             for day from (- (- chronometrist-spark-length 1)) to 0
             collect
             (setq duration
                   (chronometrist-task-time-one-day task
                                        (ts-adjust 'day day today)))
             into durations
             unless (zerop duration) do (setq active-p t)
             finally return
             (if (and active-p chronometrist-spark-show-range)
                 (format "%s %s" (spark durations) (chronometrist-spark-range durations))
               (format "%s" (spark durations))))))
    (list task (vconcat vector `[,sparkline]))))

TODO schema-transformer   function

calculate length while accounting for space

(defun chronometrist-spark-schema-transformer (schema)
  "Add a sparkline column to SCHEMA.
Used to add a sparkline column to `chronometrist-schema-transformers'.
SCHEMA should be a vector as specified by `tabulated-list-format'."
  (vconcat schema `[("Graph"
                     ,(if chronometrist-spark-show-range
                        (+ chronometrist-spark-length 12)
                        chronometrist-spark-length)
                     t)]))

setup   writer

(defun chronometrist-spark-setup ()
  "Add `chronometrist-sparkline' functions to `chronometrist' hooks."
  (add-to-list 'chronometrist-row-transformers     #'chronometrist-spark-row-transformer)
  (add-to-list 'chronometrist-schema-transformers  #'chronometrist-spark-schema-transformer))

teardown   writer

(defun chronometrist-spark-teardown ()
  "Remove `chronometrist-sparkline' functions from `chronometrist' hooks."
  (setq chronometrist-row-transformers
        (remove #'chronometrist-spark-row-transformer chronometrist-row-transformers)
        chronometrist-schema-transformers
        (remove #'chronometrist-spark-schema-transformer chronometrist-schema-transformers)))

minor-mode   minor mode

(define-minor-mode chronometrist-spark-minor-mode
  nil nil nil nil
  ;; when being enabled/disabled, `chronometrist-spark-minor-mode' will already be t/nil here
  (if chronometrist-spark-minor-mode (chronometrist-spark-setup) (chronometrist-spark-teardown)))

task graph view

prompt

task-durations-month

(defun chronometrist-task-durations-month (task iso-year-month)
  (let* ((start-ts  (chronometrist-iso-date-to-ts (concat iso-year-month "-01")))
         ;; last date of month
         (stop-ts   (ts-adjust 'month 1 'day -1 start-ts)))
    (cl-loop with current = start-ts
      while (not (ts> current stop-ts))
      collect (chronometrist-task-time-one-day task current)
      do (setq current (ts-adjust 'day 1 current)))))
tests
(ert-deftest chronometrist-task-durations-month ()
  (should
   (--every-p #'integerp (chronometrist-task-durations-month "Anything" "2021-01"))))

task-graph   custom group

(defgroup chronometrist-task-graph nil
  "Task-specific monthly/weekly graph for the `chronometrist' time tracker."
  :group 'chronometrist)

buffer-name   custom variable

(defcustom chronometrist-task-graph-buffer-name "*chronometrist-task-graph*"
  "Name of buffer created by `chronometrist-task-graph'."
  :type 'string)

default-unit   custom variable

(defcustom chronometrist-task-graph-default-unit :month
  "Default unit for `chronometrist-task-graph'.
Values may be either `:month' or `:week'."
  :type '(radio (const :tag "Month" :month) (const :tag "Week" :week)))

schema   custom variable

(defcustom chronometrist-task-graph-schema
  [("#" 3 (lambda (row-1 row-2) (< (car row-1) (car row-2))))
   ("Month" 20 t)
   ("Graph" 31 t)]
  "Vector specifying format of `chronometrist-task-graph' buffer.
See `tabulated-list-format'."
  :type '(vector))

schema-transformers   extension variable

(defvar chronometrist-task-graph-schema-transformers nil
  "List of functions to transform `chronometrist-task-graph-schema' (which see).
This is passed to `chronometrist-run-transformers', which see.

Extensions adding to this list to increase the number of columns
will also need to modify the value of `tabulated-list-entries' by
using `chronometrist-task-graph-row-transformers'.")

row-transformers   extension variable

(defvar chronometrist-task-graph-row-transformers nil
  "List of functions to transform each row of `chronometrist-task-graph-rows'.
This is passed to `chronometrist-run-transformers', which see.

Extensions adding to this list to increase the number of columns
will also need to modify the value of `tabulated-list-format' by
using `chronometrist-task-graph-schema-transformers'.")

months   variable

(defvar chronometrist-task-graph-months
'("January" "February" "March" "April" "May" "June"
  "July" "August" "September" "October" "November" "December"))

rows   function

  1. check default unit
(defun chronometrist-task-graph-rows ()
  "Return rows to be displayed in the `chronometrist-task-graph' buffer.
Return value is a list as specified by `tabulated-list-entries'."
  (cl-loop with index = 1
    for month in chronometrist-task-graph-months collect
    (-let* (((&plist :name name :tags tags :start start :stop stop) plist)
            )
      (--> (vconcat (vector index-string name)
                    (when chronometrist-task-graph-display-tags (vector tags))
                    (when chronometrist-task-graph-display-key-values (vector key-values))
                    (vector duration timespan))
        (list index it)
        (chronometrist-run-transformers chronometrist-task-graph-row-transformers it)))
    do (cl-incf index)))

chronometrist-task-graph-mode   major mode

(define-derived-mode chronometrist-task-graph-mode tabulated-list-mode "Task Graph"
  "Major mode for `chronometrist-task-graph'."
  (make-local-variable 'tabulated-list-format)
  (--> (chronometrist-run-transformers chronometrist-task-graph-schema-transformers
                           chronometrist-task-graph-schema)
    (setq tabulated-list-format it))
  (make-local-variable 'tabulated-list-entries)
  (setq tabulated-list-entries #'chronometrist-task-graph-rows)
  (make-local-variable 'tabulated-list-sort-key)
  (tabulated-list-init-header)
  (run-hooks 'chronometrist-task-graph-mode-hook))

chronometrist-task-graph   command

(defun chronometrist-task-graph ()
  (interactive)
  (let ((buffer (get-buffer-create chronometrist-task-graph-buffer-name))
        (window (save-excursion
                  (get-buffer-window chronometrist-task-graph-buffer-name t))))
    (cond (window (kill-buffer chronometrist-task-graph-buffer-name))
          (t (with-current-buffer buffer
               (switch-to-buffer buffer)
               (chronometrist-task-graph-mode)
               (tabulated-list-print))))))

Provide

(provide 'chronometrist-spark)
;;; chronometrist-spark.el ends here