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.el

191 lines
8.1 KiB
EmacsLisp

;;; 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:
;;
;; This package adds a column to Chronometrist displaying sparklines for each task.
;;; Code:
(require 'chronometrist)
(require 'spark)
(defgroup chronometrist-spark nil
"Show sparklines in `chronometrist'."
:group 'applications)
(defcustom chronometrist-spark-length 7
"Length of each sparkline in number of days."
:type 'integer)
(defcustom chronometrist-spark-show-range t
"If non-nil, display range of each sparkline."
:type 'boolean)
(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))))))
(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]))))
(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)]))
(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))
(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)))
(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)))
(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)))))
(defgroup chronometrist-task-graph nil
"Task-specific monthly/weekly graph for the `chronometrist' time tracker."
:group 'chronometrist)
(defcustom chronometrist-task-graph-buffer-name "*chronometrist-task-graph*"
"Name of buffer created by `chronometrist-task-graph'."
:type 'string)
(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)))
(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))
(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'.")
(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'.")
(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 plist in (gethash (chronometrist-events-last-date) chronometrist-events) 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)))
(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))
(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 'chronometrist-spark)
;;; chronometrist-spark.el ends here