12 KiB
12 KiB
chronometrist-spark
- Library headers and commentary
- Dependencies
- Code
- Provide
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
- 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