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>

My approach might be too radically different for you. But I would break this problem down differently.

I would have core items with tags of Trip, Person, and Email. I would also have a collection of Events of different types, say Birthday, First Smile, Email Sent (likely also Email Received for incoming ones), and Trip Start and Trip End.

Each of those Event subtype items would have two crucial fields: item and when. Each event type might also add more custom fields, but just these two will allow us to build the sort of UI you describe.

With wikitext like this:

<$list filter="[tags[]tag[Event]tagging[]get[when]] :map[split[]first[6]join[]] +[unique[]sort[]]" variable="month">
<div class="month-events">
  <h3><$text text={{{ [<month>addsuffix[01120000000]format:date[MMM, YYYY]] }}} /></h3>
  <ul>
    <$list filter="[tags[]tag[Event]tagging[]] :filter[get[when]prefix<month>] +[sort[when]]"  >
      <li class="event-line">
        <span class="event-date"><$view field="when" format="date" template="ddd " /><$link><$view field="when" format="date" template="DDth" /></$link>: </span>
        <span class=`${ [<currentTiddler>tags[]tag[Event]slugify[]join[ ]] }$`>
          <span class="event-type"><$text text={{{ [<currentTiddler>tags[]tag[Event]] }}} />:</span>
          <span class="item-link"><$link to={{!!item}}/></span>
        </span>
      </li>
    </$list>
  </ul>
</div>
</$list>

We could end up generating something like this:

<div class="month-events">
  <h3>January, 2026</h3>
  <ul>
    <li class="event-line">
      <span class="event-date">Thu <a class="tc-tiddlylink tc-tiddlylink-resolves" href="#Event%2F5">1st</a>: </span>
      <span class="birthday">
        <span class="event-type">Birthday:</span>
        <span class="item-link"><a class="tc-tiddlylink tc-tiddlylink-resolves" href="#Charlie">Charlie</a></span>
      </span>
    </li>
  </ul>
</div>

<div class="month-events">
  <h3>February, 2026</h3>
  <ul>
    <li class="event-line">
      <span class="event-date">...</span>
      <span class="email-sent">...</span>
    </li>
    <li class="event-line">
      <span class="event-date">...</span>
      <span class="email-sent">...</span>
    </li>
  </ul>
</div>

<div class="month-events">
  <h3>March, 2026</h3>
  <ul>
    <li class="event-line">...</li>
    <li class="event-line">...</li>
    <li class="event-line">...</li>
  </ul>
</div>

which looks like this:

You can play with this solution by downloading this and dragging the resulting file to any wiki:

EventList.json (233.3 KB)

Then poke around the main tiddlers, such as EventList, Trip to the Mountains, Charlie, etc, and also the tag tiddlers such as First Smile and Trip End, and finally the individual events such as Event/3 to see how they work. There are two ViewTemplates in the $:/my-stuff namespace. One for Tag Tiddlers such as Birthday, and the other for main items like Email to Bob and Charlie. Both just add footers with lists of related events.

The layout doesn’t really try to mimic yours; I would leave that to you. I also didn’t add your icons, but I gave plenty of class names in the output to hook in any CSS you like.

It’s very different from your multiple event fields per tiddler. While I’m sure we could work something out with that, I thought you might at least be interested in a different approach, one which I find more flexible.

1 Like

Thanks so much for your reply, @Scott_Sauyet.

I played with your solution for a while yesterday, and after giving it some thought I have come to agree with your basic approach. It’s like using join tables in a relational database: there’s a lot more flexibility, although some more work is needed to retrieve all the relevant information for a specific use case.

Ironically, by the time I read your comment I had already built a sandbox solution that involved some field concatenation and then splitting around a separator token, which solved my issue, but which is definitely an inelegant approach, and still much less flexible.

In terms of the interface, I’m more or less staying with what I used before. But your code and conceptual approach have been helpful!

Thanks again :slight_smile:

Exactly. And I meant to mention that.

I was sure something like that was possible, but I didn’t spend any real time trying it.

Best of luck!

2 Likes