191 lines
8.1 KiB
EmacsLisp
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
|