El Gantt – A Gantt chart/calendar for Orgmode
El Gantt creates a Gantt calendar from your orgmode files. It provides a flexible customization system with the goal of being adaptable to ?multiple purposes. You can move dates, scroll forward and backward, jump to the underlying org file, and customize the display.
This package is under ongoing development. Feature requests and bug reports are welcome. Help is welcome.
Installation
Install the dependencies:
Clone this repository into your lisp
directory:
cd ~/.emacs.d/lisp
git clone https://github.com/legalnonsense/elgantt.git
Then:
(add-to-list 'load-path (concat user-emacs-directory "lisp/elgantt/")) ;; Or wherever it is located
(require 'elgantt)
Disclaimer
This package is in-progress. I don’t know if that means it is in alpha testing, or beta, or whatever. Everything described below should work, and it is stable enough for my every day use. If this package is useful for you, please help me polish it so we can put in on MELPA.
First use
Run (elgantt-open)
. By default, Elgantt will use your org-agenda files.
Before running this on your org-agenda files, you may want to experiment with the test file.
Using the test file
Set elgantt-agenda-files
to the location wherever you installed elgantt
. If you cloned to the lisp
subdirectory, then:
(setq elgantt-agenda-files (concat user-emacs-directory "lisp/elgantt/test.org"))
And run (elgantt-open)
. You’ll see this rather bland looking chart:
Customization
All variables can get set with setq
and there is no need to use the customization interface, or use-package
’s :custom keyword.
Setting the header type
The headers of the calendar are defined by elgantt-header-type
. There are four default values and an option to define a custom function which will be run at the first point of each org heading:
Value | Behavior |
---|---|
category | Group entries by their CATEGORY property, or the filename if no CATEGORY property is set. |
hashtag | Group entries by tags which are prefixed by a hashtag. |
outline | Use the outline structure |
root | Group by the root heading |
function | Run the given function at point, grouping entries by the return value of the function |
Setting the timestamps to display
Set the variable elgantt-timestamps-to-display
to control what types of timestamps are displayed. This variable is a list which can contain any of:
- deadline
- timestamp
- timestamp-ia
- scheduled
- timestamp-range
- timestamp-range-ia
The order of the list matters determined precedence. Only the first type of entry found in a heading will be displayed with a character.
I generally use (setq elgantt-timestamps-to-display '(deadline timestamp scheduled timestamp-range))
Display options
Value | Behavior |
elgantt-startup-folded | If non-nil, display all entries, grouped by header-type, on a single line; otherwise, entries are grouped under a header with one entry per line |
elgantt-show-header-depth | If elgantt-header-type is set to ‘outline, then show the outline depth by inserting elgantt-level-prefix-char. If elgantt-header-type is not ‘outline, then this has no effect |
elgantt-level-prefix-char | The character used to prefix nested entries. |
elgantt-even-numbered-line-change | This controls how much the percent the even numbered lines are offset from the background color; set to 0 if you don’t want any distinction |
elgantt-scroll-to-current-month-at-startup | Scroll to the current month at startup, or keep the calendar at the first timestamp |
elgantt-insert-blank-line-between-top-level-header | Just what it says. |
elgantt-draw-overarching-headers | Draw a line bracketing the start and end dates for the children of and top-level headers, assuming there is no date already associated with the header. |
elgantt-header-column-offset | The width of the header column. |
elgantt-header-line-format | See the section below detailing how to use this variable. |
elgantt-exclusions | This is a list of strings. Do not display any headers that appear in this list. |
elgantt-insert-header-even-if-no-timestamp | Insert the header even if there is no timestamp associated with it. |
elgantt-hide-number-line | Hides the number line that appears at the top of the calendar |
Other custom variables
Variable | Default |
---|---|
elgantt-deadline-character | ”▲ ” |
elgantt-active-timestamp-character | ”● ” |
elgantt-inactive-timestamp-character | ”⊚ ” |
elgantt-scheduled-character | ”⬟ ” |
elgantt-multiple-entry-character | ”☰ ” |
elgantt-timestamp-range-start-character | ”▶ ” |
elgantt-timestamp-range-end-character | ”◀ ” |
elgantt-cal-timestamp-range-ia-start-character | ”▷ ” |
elgantt-timestamp-range-ia-end-character | ”◁ ” |
elgantt-agenda-files | (org-agenda-files) |
elgantt-skip-archives | t |
elgantt-start-date | (concat (format-time-string “%Y-%m”) “-01”) (i.e., the current month) |
elgantt-header-column-offset | 20 |
elgantt-start-date
is probably the most important one here. This sets the cut-off date for when to ignore old entries.
Navigation commands
Key binding | Command |
---|---|
f | Move forward to next entry on the line |
n | Move backward to previous entry |
n | Move to the closest entry on the next line |
p | Move to the closest entry on the previous line |
F | Scroll forward by one month |
B | Scroll backward by one month |
M-f | Shift date at point forward one day |
M-b | Shift date at point backward one day |
c | Move calendar to current date |
space | Navigate to org heading at point |
Return | Show agenda for date at point |
Note about cells with multiple entries: If a calendar cell has multiple entries, a special character will be displayed (“☰” by default). If you try to perform a function on one of these cells (e.g., navigating to the org file, shifting a date, etc.), you will be prompted to select the entry you want to perform the operation on.
Examples
These exampes all use the test.org
file: (setq elgantt-agenda-files "~/.emacs.d/lisp/elgantt/test.org")
(or wherever your elgantt direcctory is located).
A note about colorizing the outline
The examples that follow draw a gradient between the scheduled time of an entry and the deadline of the entry. (The scheduled date is not actually shown in the calendar.) This is not included in the package and you need to use a custom macro (shown below) to do it. I took this idea from the org-gantt package. It is not included by default because it only works if you use deadlines and scheduling in a particular way. I do not use colorize my calendars this way, but it makes for a good demonstration. The code necessary to do this, and an alternative way to use colors, are discussed below when explaining the elgantt-create-display-rule
macro. If you want these colors to appear, evaluate this code and reload (i.e., C-r
) the calendar:
(setq elgantt-user-set-color-priority-counter 0)
(elgantt-create-display-rule draw-scheduled-to-deadline
:parser ((elgantt-color . ((when-let ((colors (org-entry-get (point) "ELGANTT-COLOR")))
(s-split " " colors)))))
:args (elgantt-scheduled elgantt-color elgantt-org-id)
:body ((when elgantt-scheduled
(let ((point1 (point))
(point2 (save-excursion
(elgantt--goto-date elgantt-scheduled)
(point)))
(color1 (or (car elgantt-color)
"black"))
(color2 (or (cadr elgantt-color)
"red")))
(when (/= point1 point2)
(elgantt--draw-gradient
color1
color2
(if (< point1 point2) point1 point2) ;; Since cells are not necessarily linked in
(if (< point1 point2) point2 point1) ;; chronological order, make sure they are sorted
nil
`(priority ,(setq elgantt-user-set-color-priority-counter
(1- elgantt-user-set-color-priority-counter))
;; Decrease the priority so that earlier entries take
;; precedence over later ones (note: it doesn’t matter if the number is negative)
:elgantt-user-overlay ,elgantt-org-id)))))))
Use outline structure, unfolded, with space between headers, and overarching header lines
(setq elgantt-header-type 'outline
elgantt-insert-blank-line-between-top-level-header t
elgantt-startup-folded nil
elgantt-show-header-depth t
elgantt-draw-overarching-headers t)
Same as above, but folded
(setq elgantt-header-type 'outline
elgantt-insert-blank-line-between-top-level-header nil
elgantt-startup-folded t
elgantt-show-header-depth t
elgantt-draw-overarching-headers)
Note: When two colored gradients overlap, the average of the two gradients will be used for the display. This way, you can still see both spans of time. (Though the result is not always pretty.)
Use hashtags, folded, with no spaces
(setq elgantt-header-type 'hashtag
elgantt-insert-blank-line-between-top-level-header nil
elgantt-startup-folded t)
What does it look like unfolded?
A custom header
Here’s a silly example that will group headers by the first letter ofo the headline
(setq elgantt-header-type (lambda () (substring (org-entry-get (point) "ITEM") 0 1)))
;; You’ll also want to set `elgantt-insert-header-even-if-no-timestamp' to nil, otherwise you’ll see single letter headers that are assocated with headlines without dates
Header line format
The variable elgantt-custom-header-line
controls the format of the header line. It can use any of the properties that are in a cell. You can reference these properties with the :prop
keyword, with or without the :elgantt-
prefix. (For example, you can access the headline of a cell’s entry with :elgantt-headline
or headline
. There is also a unique property date-at-point
which will display the date at point and that is not dependent on the properties stored in the given cell. If there are multiple entries in the cell, then the data will be separated with a pipe (i.e, |
). You can align text in the headerline to the left, center, or right side of the header. If there is an overlap, the latter properties will take precedence over the former. If the property doesn’t return a string, it will be formatted into a string with (format "%s")
.
The :prop
keyword can also be a function that is run at the cell at point.
The header line is disabled by default while I finish sorting out the variable and function that handles it. You can enable it with (setq header-line-format '(:eval (elgantt-header-line-function)))
. This will use the default value for elgantt-custom-header-line
, which is:
(setq elgantt-custom-header-line '((:left ((:prop date-at-point
:padding 25)
(:prop headline
:padding 25)))))
Here is another example:
(setq elgantt-custom-header-line '((:left ((:prop date-at-point
;; you could also use, for example, 'elgantt-get-date-at-point
;; or (lambda () (elgantt-get-date-at-point))
:padding 25)
(:prop todo
:padding 30)))
(:center ((:prop headline)))
(:right ((:prop hashtag
:padding 40
:text-props (face (:background "red")))))))
The header line is work-in-progress and this was my first attempt at a solution. Here is a list of all the properties:
Keyword | Description |
---|---|
:prop | Any property stored in a cell that is retrievable with elgantt-get-prop-at-point , or a function that is run at the cell at point |
:padding | Integer which indicates the padding before the next entry (defaults to no padding) |
:after-pad | If the length of the string exceeds the value of :padding, still separate this entry from the following by this number of padding characters |
:padding-char | The character used for padding. Can be any single character. Defaults to a space |
:text-props | Any text properties associated with the text. For example, you can set a custom face with (face '(:background "red")) |
Macro/configuration examples and explanations
Elgantt aims to provide a flexible way to customize calendar displays. Whether it hits its target is not my concern.
elgantt-create-display-rule
macro
The This macro is used to customize the display of the calendar. It defines functions that are run at each cell after the calendar is generated. If a cell contains multiple entries, it will be run for each entry in the cell.
Accessing and adding properties
Before proceeding, here is a list of the properties that are included for each entry in the calendar:
The following properties are included in each cell by default:
Property | Value |
---|---|
:elgantt-headline | Text of the org headline (no text properties) |
:elgantt-deadline | Deadline as a string YYYY-MM-DD, or nil |
:elgantt-scheduled | Scheduled timestamp, or nil |
:elgantt-timestamp | First active timestamp (date only) or nil |
:elgantt-timestamp-ia | First inactive timestamp (date only) or nil |
:elgantt-timestamp-range | Active timestamp range, as a list of two strings ‘(“YYYY-MM-DD” “YYYY-MM-DD”) or nil |
:elgantt-timestamp-range-ia | Same, but inactive timestamp range |
:elgantt-category | Category property of the heading, or the filename if no category property is supplied |
:elgantt-todo | TODO type, no properties, or nil |
:elgantt-marker | Marker pointing to the location of the heading in the org buffer |
:elgantt-file | Filename of the underlying org file |
:elgantt-org-buffer | Buffer for the underlying org heading |
:elgantt-alltags | A list of all tags, including inherited tags, associated with the heading |
:elgantt-header | Header used for insertion into the calendar buffer. Depends on the value of elgantt-header-type |
:elgantt-date | Date used for insertion into the calendar. Uses the first date found in elgantt-timestamps-to-display |
:elgantt-hashtag | Any hashtag (inherited) associated with the headline |
All properties returned by (org-entry-properties)
are also included in an entry’s property list.
Here are some basic examples of how to use the display customization macro.
Changing the color of certain cells
Suppose we want to change the background color of any cell with a “TODO” state to red:
(elgantt-create-display-rule turn-todo-red
:args (elgantt-todo) ;; Any argument in this list is available in the body
:body ((when (string= "TODO" elgantt-todo)
;; `elgantt--create-overlay' is generally the easiest way to create an overlay
;; since `ov' is not a dependency.
(elgantt--create-overlay (point) (1+ (point))
'(face (:background "red"))))))
Some caveats: If there is already an overlay on the cell, you have to manage the overlay priorities for them to display properly. The manual is serious when it warns “you should not make assumptions about which overlay will prevail” when two overlays share the same priority (or do not have a priority).
For example, here we will choose an arbitrarily large priority to make sure this overlay is displayed over any others:
(elgantt-create-display-rule turn-todo-red
:args (elgantt-todo) ;; Any argument listed here is available in the body
:body ((when (string= "TODO" elgantt-todo)
;; `elgantt--create-overlay' is generally the easiest way to create an overlay
(elgantt--create-overlay (point) (1+ (point))
'(face (:background "red")
priority 99999)))))
If you want to make a dynamic display (i.e., one that updates every time you move), the post-command-hook
keyword will add the function as a post-command-hook and run it each time the cursor moves. For example, suppose you want to make each cell red that matches the TODO state of the cell at point. We’ll use the the macro elgantt--iterate-over-cells
to run the expression for each cell.
If you want to use this kind of display, then you’ll probably want to give the overlay a unique ID, and clear those overlay each time the cursor moves.
(elgantt-create-display-rule turn-matching-todos-red
:args (elgantt-todo)
:post-command-hook t ;; This will recalculate every time the point moves
:body ((remove-overlays (point-min) (point-max) :turn-it-red t)
;; Since this will run each time the cursor moves, we need to clear
;; the previous overlays first
(when elgantt-todo ;; make sure there is a todo state
(elgantt--iterate-over-cells
(when (member elgantt-todo (elgantt-get-prop-at-point :elgantt-todo))
(elgantt--create-overlay (point) (1+ (point))
'(face (:background "red")
priority 9999
;; arbitrary identifier
;; so we know what overlays to clear
:turn-it-red t)))))))
Using the test.org file (where only a few of the headlines have TODO states), you’ll see this will turn the background of any entry that also has a TODO state when the point is on a cell with the same state:
If, during your experimentation, you want to disable a display rule, add :disable t
and it will be removed from the function stack (or the post-command hook, if appropriate). In the alternative, call elgantt--clear-all-customizations
which will delete any functions created by the customization macros.
Adding new properties from org files
Suppose you want to change the color of a cell based on a property that is not present by default. For example, you want to change the color if the cell has a certain priority, but that property is not included by default. In that case, use the :parser
keyword to add a property. The expression is run at the first point of each org heading, and will be automatically added to the parsing function. The syntax is:
:parser ((property-name1 . ((expression)))
(property-name2 . ((expression))))
So, to add the property to get the priority of an org heading:
(elgantt-create-display-rule priority-display
:parser ((elgantt-priority . ((org-entry-get (point) "PRIORITY"))))
:body (())) ;; insert code here, which can use elgantt-priority variable
You must reload the calendar after evaluating the macro so the calendar can repopulate and :elgantt-priority
and its value will be added to each entry’s text properties.
Examples
Other ways to colorize time blocks
Here is how I colorize blocks of time. It depends on two org properties: ELGANTT-COLOR
and ELGANTT-LINKED-TO
. ELGANTT-COLOR
is an org property that contains two color names, which will represent the start and end of a gradient. ELGANTT-LINKED-TO
contains the ID of an org heading. This is different than the colorizing macro used for other examples, which colors a block starting with the scheduled date and ending with a deadline.
(setq elgantt-user-set-color-priority-counter 0) ;; There must be a counter to ensure that overlapping overlays are handled properly
(elgantt-create-display-rule user-set-color
:parser ((elgantt-color . ((when-let ((colors (org-entry-get (point) "ELGANTT-COLOR")))
(s-split " " colors))))
(elgantt-linked-to . ((org-entry-get (point) "ELGANTT-LINKED-TO"))))
:args (elgantt-org-id)
:body ((when elgantt-linked-to
(save-excursion
(when-let ((point1 (point))
(point2 (let (date)
;; Cells can be linked even if they are not
;; in the same header in the calendar. Therefore,
;; we have to get the date of the linked cell, and then
;; move to that date in the current header
(save-excursion (elgantt--goto-id elgantt-linked-to)
(setq date (elgantt-get-date-at-point)))
(elgantt--goto-date date)
(point)))
(color1 (car elgantt-color))
(color2 (cadr elgantt-color)))
(when (/= point1 point2)
(elgantt--draw-gradient
color1
color2
(if (< point1 point2) point1 point2) ;; Since cells are not necessarily linked in
(if (< point1 point2) point2 point1) ;; chronological order, make sure they are sorted
nil
`(priority ,(setq elgantt-user-set-color-priority-counter
(1- elgantt-user-set-color-priority-counter))
;; Decrease the priority so that earlier entries take
;; precedence over later ones
:elgantt-user-overlay ,elgantt-org-id))))))))
elgantt--connect-cells
Linking cells with Some samples here use the following macro to draw a line through cells which share the same hashtag. This code also adds a shortcut to move to the next matching hashtag:
(elgantt-create-display-rule show-hashtag-links
:args (elgantt-hashtag)
:post-command-hook t ;; update each time the point is moved
:body ((elgantt--clear-juxtapositions nil nil 'hashtag-link) ;; Need to clear the last display
(when elgantt-hashtag ;; only do it if there is a hashtag property at the cell
(elgantt--connect-cells :elgantt-alltags elgantt-hashtag 'hashtag-link '(:foreground "red")))))
(elgantt-create-action follow-hashtag-link-forward
:args (elgantt-alltags)
:binding "C-M-f"
:body ((when-let* ((hashtag (--first (s-starts-with-p "#" it)
elgantt-alltags))
(point (car (elgantt--next-match :elgantt-alltags hashtag))))
(goto-char point))))
(elgantt-create-action follow-hashtag-link-backward
:args (elgantt-alltags)
:binding "C-M-b"
:body ((when-let* ((hashtag (--first (s-starts-with-p "#" it)
elgantt-alltags))
(point (car (elgantt--previous-match :elgantt-alltags hashtag))))
(goto-char point))))
Helper functions
The following functions are included to aid customizing the display. See docstrings for more information.
Drawing the display
elgantt--create-overlay
.
Create overlays with elgantt--draw-gradient.
Draw a gradient with elgantt--draw-progress-bar.
Draw a progress bar with Here is an example of how to use elgantt--draw-progress-bar
Suppose you have the following org file:
* TODO read The Illuminatus! Trilogy
SCHEDULED: <2020-06-02 Tue> DEADLINE: <2020-07-21 Tue>
:PROPERTIES:
:TOTAL_PAGES: 667
:PAGES_READ: 555
:ID: 99a97ef7-b555-4f98-bdd3-7e44510ac7a4
:END:
The following code:
(elgantt-create-display-rule pages-read-progress
:parser ((total-pages . ((string-to-number
(org-entry-get (point) "TOTAL_PAGES"))))
(pages-read . ((string-to-number
(org-entry-get (point) "PAGES_READ")))))
:args (elgantt-deadline elgantt-scheduled)
:body ((when (and elgantt-deadline elgantt-scheduled
total-pages pages-read)
(let* ((start (progn (elgantt--goto-date elgantt-scheduled)
(point)))
(end (progn (elgantt--goto-date elgantt-deadline)
(point)))
(percent (/ (float pages-read)
(float total-pages))))
(elgantt--draw-progress-bar "red" "blue"
start
end
percent)))))
Will automatically display a progress bar starting at the scheduled date, to the deadline date, displaying a progress bar that represents the percent of pages read:
Note: the above code will generate an error if it is run on an org file that does not have the “TOTAL_PAGES” and “PAGES_READ” properties, because org-entry-get
will return nil, which will cause string-to-number
to fail. Instead, you should do something like:
:parser ((total-pages . ((--when-let (org-entry-get (point) "TOTAL_PAGES")
(string-to-number it))))
(pages-read . ((--when-let (org-entry-get (point) "PAGES_READ")
(string-to-number it)))))
Or some other solution if you don’t like dash
.
elgantt--draw-line
. See also elgantt--connect-cell
.
Draw a line from one cell to another with elgantt--insert-juxtaposition
and clear them with elgantt--clear-juxtapositions
.
Juxtapose text on top of a cell with elgantt--change-char
.
Change the character of a cell (while preserving text properties) with Navigating the buffer
elgantt--goto-id
.
Move to a cell by org-id with elgantt--goto-date
.
Move to a date on the current line with elgantt--iterate-over-cells
Iterate over all entries with Selecting from multiple entries
Some cells will have multiple entries. To prompt the user to pick which one should be used: elgantt--select-entry
.
Getting calendar data
elgantt-get-date-at-point
.
To get the date at point: elgantt-get-prop-at-point
.
To get the properties of a cell: This will always return a list, and if there are multiple entries in the cell at point it will list all values. Without any arguments, it will return all properties.
Editing the underlying org file
elgantt-with-point-at-orig-entry
to execute code at the underlying org heading.
Use the macro Redrawing
You can’t reload a single cell because doing so invites catastrophe. But you can update all cells for the date at point: elgantt-update-this-cell
.
The display (i.e., overlays) of a single cell can be redrawn with elgantt--update-display-this-cell
or all cells with elgantt--update-display-all-cells
.
If all else fails, reload everything with elgantt-open
.
A note about org-ql: Org-ql creates a cache of its results and uses that cache until the underlying org file is changed. If you change something about the way the calendar is displayed, odds are that there will be a problem with using the org-ql cache. For this reason, all reloading invalidates the org-ql cache by calling elgantt--reset-org-ql-cache
which simply sets org-ql-cache
to its initial value. This seems to solve reloading problems.
Creating custom views
You can create custom views of the gantt chart/calendar by defining a function like this. Don’t try to let-bind the variables and then call elgantt-open
open inside the closure; things will break. You can use setq
and do not need to use the customize interface.
(defun elgantt-outline-folded ()
(interactive)
(setq elgantt-start-date nil
elgantt-scroll-to-current-month-at-startup nil
elgantt-agenda-files "~/.emacs.d/lisp/elgantt/test.org"
elgantt-startup-folded nil
elgantt-insert-header-even-if-no-timestamp t
elgantt-header-type 'outline
elgantt-show-header-depth t
elgantt-header-column-offset 30
elgantt-even-numbered-line-change 5)
(elgantt-open))
If you want to use custom display macros, then you should call (elgantt--clear-all-customizations)
and then include your custom macros inside the function.
Faces and themes
Elgantt should adjust its colors to work with your theme, regardless of whether it is dark or light.
Interacting with the calendar
There are two ways to interact with the calender: the elgantt-create-action
macro and the separate module, elgantt-interaction
.
elgantt-create-action
This macro works the same way as elgantt-create-display-rule
except that has keywords for binding
commands. I don’t use this macro for anything, but you could use it to perform actions on the org-file from the calendar (e.g., marking a TODO as DONE).
elgantt-interaction
To use this, you must (require 'elgantt-interaction)
.
This module experimental. The code is not cleaned up. It was written in a frenzy of wondering whether I could without considering whether I should. If this inspires ideas for others to use it, I will return to it. Otherwise, unless I have a need, I plan to abandon it.
Here is an example I use to set the :ELGANTT-LINKED-TO
and :ELGANTT-COLOR
property used in the example above. It is designed to allow the user to select cells and perform actions on them in a certain sequence. Here, it allows the user to make two selections, and when return is pressed, it will prompted the user to enter two colors, and then set the properties of the relevant org heading.
While this example works, the code in elgantt-interaction
is generally untested. I do not know whether I will develop it further absent a need to do so. The framework, in theory, provides a robust way to create ways to interact with the calendar and perform actions on multiple org entries.
To invoke the interface, press a
to be prompted to select which interface you’d like to execute. After that, a counter should appear which shows the number of cells selected. The message displayed is defined by the :selection-messages
keyword. Once the cells are selected (by pressing space
), the user presses Return
to execute the command. The execution functions will be run in the order listed in :execution-functions
. The first number refers to cells in the order in which they were selected. The variable return-val
is the return value of the previous function.
So, here, the user selects two cells and presses return. Then, the program moves to the second selected cell, and runs org-id-get-create
, and returns the value. The section function moves to the first cell that the user selected, and adds the ID of the second selection (i.e., return-val
), and then prompts the user for two colors and sets the properties of that heading appropriatly.
In addition to being able to use numbers to refer to cells by the order in which they were selected, you can use all
, rest
, all-but-last
, and last
to refer to the cells and perform operations on them.
(require 'elgantt-interaction)
(elgantt--selection-rule
:name colorize
:selection-number 2
:selection-messages ((1 . "Select first cell")
(2 . "Select second cell"))
:execution-functions ((2 . ((elgantt-with-point-at-orig-entry nil
(org-id-get-create))))
(1 . ((elgantt-with-point-at-orig-entry nil
(org-set-property "ELGANTT-LINKED-TO" return-val)
(org-set-property "ELGANTT-COLOR" (concat (s-trim (read-color "Select start color:"))
" "
(s-trim (read-color "Select end color:")))))))))
;; You’ll also need to use this to colorize
(setq elgantt-user-set-color-priority-counter 0) ;; There must be a counter to ensure that overlapping overlays are handled properly
(elgantt-create-display-rule user-set-color
:parser ((elgantt-color . ((when-let ((colors (org-entry-get (point) "ELGANTT-COLOR")))
(s-split " " colors))))
(elgantt-linked-to . ((org-entry-get (point) "ELGANTT-LINKED-TO"))))
:args (elgantt-org-id)
:body ((when elgantt-linked-to
(save-excursion
(when-let ((point1 (point))
(point2 (let (date)
;; Cells can be linked even if they are not
;; in the same header in the calendar. Therefore,
;; we have to get the date of the linked cell, and then
;; move to that date in the current header
(save-excursion (elgantt--goto-id elgantt-linked-to)
(setq date (elgantt-get-date-at-point)))
(elgantt--goto-date date)
(point)))
(color1 (car elgantt-color))
(color2 (cadr elgantt-color)))
(when (/= point1 point2)
(elgantt--draw-gradient
color1
color2
(if (< point1 point2) point1 point2) ;; Since cells are not necessarily linked in
(if (< point1 point2) point2 point1) ;; chronological order, make sure they are sorted
nil
`(priority ,(setq elgantt-user-set-color-priority-counter
(1- elgantt-user-set-color-priority-counter))
;; Decrease the priority so that earlier entries take
;; precedence over later ones
:elgantt-user-overlay ,elgantt-org-id))))))))
Here is a second example I played with previously, which provided a more advanced way to link cells/headings together. You can see the use of return-val
being passed from one execution function to the next. This is included only for the purposes of illustrating how to use the macro.
(elgantt--selection-rule :name set-anchor
:parser ((:elgantt-dependents . ((when-let ((dependents (cdar (org-entry-properties (point)
"ELGANTT-DEPENDENTS"))))
(s-split " " dependents)))))
:execution-functions ((2 . ((elgantt-with-point-at-orig-entry nil
(org-id-get-create))))
(1 . ((elgantt-with-point-at-orig-entry nil
(let ((current-heading-id (org-id-get-create)))
(org-set-property "ELGANTT-DEPENDENTS"
(format "%s"
(substring
(if (member return-val elgantt-dependents)
elgantt-dependents
(push return-val elgantt-dependents))
1 -1)))))))
(2 . ((elgantt-with-point-at-orig-entry nil
(org-set-property "ELGANTT-ANCHOR" return-val)))))
:selection-messages ((1 . "Select the anchor.")
(rest . "Select the dependents."))
:selection-number 0)
This was previously accompanied by code that allowed the user to move the date of dependent cells by moving the anchor cell, and which highlighted all dependent cells when the point was on an anchor. I abandoned this for various reasons. If there is interest in this level of interface I can clean it up and get it working.
FAQ
Your code…
I’ll save you the trouble:
This is a hobby and a continued exercise in learning elisp and programming, and I realized a lot of things along the way. Mostly, I realized that programming is not as much fun as I thought it was, and takes way more time than it should. I don’t have the patience to clean up the code like I should. There are byte-compile warnings. I do not care.
I originally wrote that I hoped publishing this would get it out of my life, but it seems there is interest so I will push this as far as my time and ability will allow. If you can help, please help.
Can you fold and unfold without reloading?
Not without significant changes to the code, or breaking other existing features. You’ll just have to change the value of elgantt-startup-folded
and reload.
Why so many gradients?
They are pretty. You can also customize where the midpoint of the gradient appears so it reflects remaining time. If you don’t like gradients, then just use the same start and end color.
Change log
[2020-07-31 Fri]
Added elgantt-custom-header-line
and elgantt--header-line-formatter