Sorting tiddlers: how to include the same tiddler once for each of a few different fields (using sortsub)

Hello :slight_smile:

I’ve been working on a Timeline tiddler, to list some different tiddlers chronologically. Different types of information must be sorted according to different fields:

  • an email (tiddler with tag Email) must use the created field
  • a trip (tiddler with tag Trip) must use the start_at field
  • a person (tiddler with field birthday) must use the birthday field

I’m using the sortsub function to determine which field to use when sorting:

\define timeline.compare-start-at-or-created()
[<currentTiddler>get[birthday]] :else[<currentTiddler>get[start_at]] :else[get[created]]
\end

<!-- Sort tiddlers -->
\define timeline.tiddlers-sort-filter()
[<timeline.sort-direction>match[ascending]] :then[sortsub:date<timeline.compare-start-at-or-created>] :else[!sortsub:date<timeline.compare-start-at-or-created>] :filter[!match[ascending]]
\end

This seems to work quite well, as you can see in the screenshot. However, the problem I have is when a tiddler has more than one field, where I’d like it to show up in the timeline for each of those fields. For example, Charlie’s birthday is in January, so I want her to show up there. But her first smile was in March, so I want her to show up there, too. I (also) want to be able to identify, during the sorting, the reason that the tiddler is showing up there – in other words, which field is being used here. This will also help me to identify which icon to use, so for a birthday there will be one icon, and for a first smile there will be a different icon.

To be very clear, I want to see ‘Charlie born’ on 1st January, and ‘Charlie’s first smile’ on 1st March – even though this data comes from the same single tiddler.

I’m struggling to conceptualize this in terms of filters, as the sortsub function gets a single tiddler, and doesn’t have the context to know which specific field to use this time for this particular tiddler.

One idea that I had would be to somehow create a ‘virtual’ tiddler for each of the relevant fields of the single tiddler, eg. ‘Charlie born + birthday field’ and ‘Charlie first smile + first-smile field’, but I’m not sure if that’s possible. The issue would be that I would still need the original title of the tiddler, ie. Charlie + her birthday, Charlie + the date of her first smile.

The code I’m sharing here is simplified, but might include some remnants of the more complex code I’m actually using to cover a variety of different tiddler ‘types’.

While we’re on the same topic, it would be great to be able to show, for example, a date for the start of a trip, and also the date of the end of the trip as a separate timeline item. This is useful for longer trips, or other kinds of entities with a longer duration.

Your help would be appreciated! :slight_smile: I would also appreciate your insight into improving my code in general.

Data tiddlers

[
  {
    "created": "20260210134652644",
    "text": "",
    "tags": "Trip",
    "title": "Trip to the Mountains",
    "modified": "20260210134744546",
    "start_at": "20260315120000000",
    "end_at": "20260330120000000"
  },
  {
    "created": "20260207120000000",
    "text": "Hello, Bob",
    "tags": "Email",
    "title": "Email to Bob",
    "modified": "20260210140847538"
  },
  {
    "created": "20260210133918858",
    "text": "Hello, Alice",
    "tags": "Email",
    "title": "Email to Alice",
    "modified": "20260210133933321"
  },
  {
    "created": "20260210132256585",
    "text": "",
    "tags": "",
    "title": "Charlie",
    "modified": "20260210134624257",
    "birthday": "20260101120000000",
    "first-smile": "20260310120000000"
  }
]

$:/my-stuff/macros/timeline

\define timeline.compare-start-at-or-created()
[<currentTiddler>get[birthday]] :else[<currentTiddler>get[start_at]] :else[get[created]]
\end

<!-- ####### SORT FILTERS ####### -->
<!-- Sort months -->
\define timeline.months-sort-filter()
[<timeline.sort-direction>match[ascending]] :then[sort[]] :else[!sort[]] :filter[!match[ascending]]
\end

<!-- Sort tiddlers -->
\define timeline.tiddlers-sort-filter()
[<timeline.sort-direction>match[ascending]] :then[sortsub:date<timeline.compare-start-at-or-created>] :else[!sortsub:date<timeline.compare-start-at-or-created>] :filter[!match[ascending]]
\end

<!-- Filter: `title` must match timeline.year -->
\define timeline.tiddlers.title-year-filter()
[{!!title}format:date[YYYY]match<timeline.year>]
\end

<!-- Filter: `birthday` must match timeline.year -->
\define timeline.tiddlers.birthday.year-filter()
[{!!birthday}format:date[YYYY]match<timeline.year>]
\end

<!-- Filter: `created` must match timeline.year -->
\define timeline.tiddlers.created.year-filter()
[{!!created}format:date[YYYY]match<timeline.year>]
\end

<!-- Filter: `start_at` must match timeline.year -->
\define timeline.tiddlers.start_at.year-filter()
[{!!start_at}format:date[YYYY]match<timeline.year>]
\end

<!-- ###### TIDDLERS TO SHOW ###### -->
<!-- Tiddlers: birthdays -->
\procedure timeline.tiddlers.birthdays()
[<timeline.year>!match[]] :then[has[birthday]filter<timeline.tiddlers.birthday.year-filter>else[]] :else[has[birthday]]
\end

<!-- Tiddlers: writings -->
\procedure timeline.tiddlers.writings()
[<timeline.year>!match[]] :then[subfilter<timeline.writings.all>filter<timeline.tiddlers.created.year-filter>else[]] :else[subfilter<timeline.writings.all>]
\end

<!-- Tiddlers: `start_at` (eg. Stays, Trips, Visits) -->
\procedure timeline.tiddlers.start_at()
[<timeline.year>!match[]] :then[has[start_at]filter<timeline.tiddlers.start_at.year-filter>else[]] :else[has[start_at]]
\end

<!-- ###### GET DATES FROM MATCHING TIDDLERS ###### -->
<!-- Used to determine which months have content (for this year) -->
<!-- Dates from birthdays (this year) -->
\define timeline.dates.birthdays()
[<timeline.year>!match[]] :then[has[birthday]get[birthday]filter<timeline.tiddlers.title-year-filter>else[]] :else[has[birthday]]
\end

\define timeline.writings.all()
[tag[Email]] [tag[Article]]
\end

<!-- Dates from writings (this year) -->
\define timeline.dates.writings()
[<timeline.year>!match[]] :then[subfilter<timeline.writings.all>get[created]filter<timeline.tiddlers.title-year-filter>else[]] :else[subfilter<timeline.writings.all>get[created]]
\end

<!-- Dates from events (this year) (Trips, Stays, Visits, etc.) -->
\define timeline.dates.start_at()
[<timeline.year>!match[]] :then[has[start_at]get[start_at]filter<timeline.tiddlers.title-year-filter>else[]] :else[has[start_at]get[start_at]]
\end


<!-- Get all months into which the tiddlers fall,
  in the format YYYY0MM, eg. 202511. -->
\procedure timeline.months()
[subfilter<timeline.dates.start_at>] [subfilter<timeline.dates.writings>] [subfilter<timeline.dates.birthdays>] +[format:date[YYYY0MM]unique[]subfilter<timeline.months-sort-filter>] :filter[{!!title}!prefix[0000]]
\end

<!-- Get all timeline tiddlers falling in the given <year-month> -->
\procedure timeline.filterForMonth()
[subfilter<timeline.input-tiddlers>] :filter[subfilter<timeline.compare-start-at-or-created>prefix<year-month>] +[subfilter<timeline.tiddlers-sort-filter>]
\end

\procedure full-date-from-short-title()
<<currentTiddler>>120000000
\end

\procedure timeline.input-tiddlers()
{{{ [subfilter<timeline.tiddlers.writings>] }}} {{{ [subfilter<timeline.tiddlers.birthdays>] }}} {{{ [subfilter<timeline.tiddlers.start_at>] }}}
\end


<!-- Sort timeline tiddlers, using the field for each according
  to timeline.compare-start-at-or-created.
  If a tiddler has start_at, use that field to sort.
  If it doesn't, use the created field to sort. -->
\procedure timeline.filter()
[subfilter<timeline.input-tiddlers>] +[subfilter<timeline.tiddlers-sort-filter>]
\end

\procedure timeline.icon()
<span class="flex ml-1 mr-1 justify-content-flex-end">
<%if [<currentTiddler>tag[Trip]] [<currentTiddler>has[trip]] %>🧳<%endif%>
<%if [<currentTiddler>tag[Email]] %>📨<%endif%>
<%if [<currentTiddler>tag[Article]] %>📃<%endif%>
</span>
\end

Timeline tiddler

\import [[$:/my-stuff/macros/timeline]]

<style>
	.grid {
	    display: grid;
	    grid-gap: 0.3em;
	}
	.grid-col-auto-auto-1fr {
	    grid-template-columns: auto auto 1fr;
	 }
	.mb-2 { margin-bottom: 1.3em; }
	.ml-3 { margin-left: 2.5em; }
	.flex { display: flex; }
	.text-align-end: { text-align: end; }
	.timeline-item {
	    display: flex;
	    padding: 0.3em;
	    border: solid black 1px;
	    border-radius: 0.3em;
	}
</style>

<$let timeline.year="2026" timeline.sort-direction="ascending">


<!-- Iterate months -->
<$list filter=<<timeline.months>> emptyMessage="No items found">
  <$wikify name=year-month text={{!!title}}>
    <$let full-date=<<full-date-from-short-title>>>
      <$wikify name=full-date-string text=<<full-date>>>
        <!-- Heading: month, eg. November -->
          <h3 class="ml-3"><$text text={{{ [<full-date-string>format:date[MMM]] }}}/></h3>
      </$wikify>
    </$let>

<!-- Entries for month -->
<div class="grid grid-col-auto-auto-1fr mb-2">
  <$list filter=<<timeline.filterForMonth>>>
	<$wikify name=date text={{{ [<currentTiddler>subfilter<timeline.compare-start-at-or-created>format:date[ddd DDth]] }}}>
	<$wikify name=short-date text={{{ [<currentTiddler>subfilter<timeline.compare-start-at-or-created>format:date[DDth]] }}}>
	  <span class="text-align-end"><<date>>:</span>
	  <<timeline.icon>>
	  <div class="flex">
	    <div>
	      <$link />
		  <% if [<currentTiddler>has[end_at]] %>
  &mdash; until <$view field=end_at format=date template="MMM DDth, YYYY" />
		  <% endif %>
		</div>
	  </div>
	</$wikify>
	</$wikify>
  </$list> <!-- /Entries for month -->
</div><!-- /grid -->

  </$wikify> <!-- year-month -->
</$list> <!-- /Iterate months -->
</$let>