When you’re working with a list of records, a table is usually enough. You can see the important fields, sort the records, apply filters, and quickly find what you’re looking for.
But tables aren’t always the best way to understand time.
For example, imagine an opportunity that was created in January and is expected to close in March. A table can show you the two dates, but it doesn’t immediately tell you how long the opportunity has been active.
The same applies to leads, campaigns, projects, tasks, or any other records that have a start and end date. When the duration matters, seeing those dates as a visual timeline can make the information much easier to understand.
That’s where Horilla’s HorillaTimelineView becomes useful.
Instead of displaying records as rows in a table, it represents them as horizontal bars on a date-based timeline. The bars can be grouped into rows, giving the view a layout similar to a Gantt chart.
The interesting part is that HorillaTimelineView doesn’t introduce an entirely separate viewing system. It builds on top of HorillaListView, so many of the things you already use in a normal list view—filters, columns, actions, permissions, and queryset handling—continue to work.
Let’s take a look at how it works and how to add one to a Horilla application.
1. Creating a Basic Timeline View
The first step is to create a view that inherits from HorillaTimelineView.
You provide the model, filtering configuration, URLs, columns, and, most importantly, the fields that define the start and end of each timeline bar.
For example, an opportunity timeline can be defined like this:
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from horilla.contrib.generics.views.timeline import HorillaTimelineView
from .models import Opportunity
from .filters import OpportunityFilter
from .views import OpportunityListView
class OpportunityTimelineView(LoginRequiredMixin, HorillaTimelineView):
model = Opportunity
view_id = "opportunity-timeline"
filterset_class = OpportunityFilter
search_url = reverse_lazy("opportunities:opportunities_list")
main_url = reverse_lazy("opportunities:opportunities_view")
enable_quick_filters = True
timeline_start_field = "created_at"
timeline_end_field = "close_date"
timeline_group_by_field = "stage"
timeline_title_field = "name"
columns = [
"name",
"amount",
"close_date",
"stage",
"opportunity_type",
]
actions = OpportunityListView.actions
col_attrs = OpportunityListView.col_attrs
Here, each opportunity is represented by a bar that starts at created_at and ends at close_date.
The bars are grouped according to the opportunity’s stage, and the opportunity name is displayed as the title of the bar.
This makes the configuration fairly straightforward: tell Horilla which fields represent the timeline, and the view takes care of the rest.
2. How HorillaTimelineView Fits Into the Existing View System
One thing that makes HorillaTimelineView convenient is that it doesn’t work independently from the existing list-view architecture.
The relationship looks like this:
HorillaListView
└── HorillaTimelineView
├── OpportunityTimelineView
└── LeadTimelineView
In other words, HorillaTimelineView extends the functionality already provided by HorillaListView.
That means you don’t have to rebuild common list functionality just because you’re creating a timeline.
The timeline view changes some defaults to suit its layout:
| Attribute | List View Default | Timeline View Default |
| template_name | “list_view.html” | “timeline_view.html” |
| bulk_select_option | True | False |
| supports_quick_filters | True | False |
| table_class / table_width | — | False |
| paginate_by | 100 | 200 |
One small detail worth knowing is that quick filters are disabled by default for the timeline. If you want them, you can simply enable them:
enable_quick_filters = True
The familiar list-view properties such as columns, actions, col_attrs, header_attrs, filterset_class, search_url, main_url, and view_id still work as expected.
This also means you can often reuse configuration from an existing list view instead of defining everything again.
For example:
actions = OpportunityListView.actions
col_attrs = OpportunityListView.col_attrs
This keeps the timeline view consistent with the regular list view.
3. Configuring the Timeline Fields
The main difference between a normal list view and a timeline view is the timeline_* configuration.
These attributes tell Horilla how each record should be displayed on the timeline.
| Attribute | Default | Purpose |
| timeline_start_field | None | Date/datetime field used as the start of the bar |
| timeline_end_field | Start field | Date/datetime field used as the end of the bar |
| timeline_fallback_end_field | None | Fallback end date when the end date is ambiguous |
| timeline_title_field | First column | Field displayed as the bar’s label |
| timeline_group_by_field | None | Field used to divide records into timeline rows |
Start field
timeline_start_field defines where the timeline bar begins.
For example:
timeline_start_field = “created_at”
This means the bar starts from the record’s creation date.
This field is required. If it isn’t configured, Horilla cannot determine where the timeline should begin and displays a timeline_error instead of rendering the bars.
End field
The timeline_end_field determines where the bar ends.
For example:
timeline_end_field = “close_date”
If you don’t provide an end field, Horilla uses the start field as the end as well. In that case, the record is displayed as a single-day bar.
Title field
The timeline_title_field determines the text displayed on the timeline bar.
For example:
timeline_title_field = “name”
This makes the opportunity name appear on its corresponding bar.
Group-by field
You can also group timeline records into separate rows.
For example:
timeline_group_by_field = “stage”
With this configuration, opportunities belonging to different stages appear in different timeline rows.
4. Which Fields Can Be Used?
Not every model field can be used for a timeline.
The start and end fields need to be date-related fields. Horilla allows DateField and DateTimeField fields for these settings.
For example:
timeline_start_field = “created_at”
timeline_end_field = “close_date”
For grouping, Horilla supports fields such as choice fields with defined choices and ForeignKey fields.
This keeps the timeline configuration meaningful. A timeline needs something that represents time for its bars and something sensible to use for grouping.
5. Letting Users Change Timeline Fields
The fields defined on the view aren’t necessarily permanent.
Horilla allows users to change the timeline’s start, end, and grouping fields through the UI.
The selected values are passed through query parameters such as:
timeline_start
timeline_end
timeline_group_by
group_by
An important detail here is that the user’s selection can also be remembered.
Once a user changes the timeline configuration, Horilla saves those settings for that user and can restore them on the next visit.
This is useful when different users need to look at the same records from different perspectives.
For example, one user might want to see:
Created Date → Close Date
while another might prefer:
Start Date → Updated Date
without requiring developers to create separate timeline views.
You can also control which fields appear in the grouping picker using:
exclude_kanban_fields
include_kanban_fields
These work in the same way as the corresponding configuration in HorillaGroupByView.
6. Grouped Rows, Record Counts, and Timeline Scale
Timeline grouping works a little differently from the grouping you see in HorillaGroupByView.
A timeline uses a single level of grouping. Each group becomes a row, or lane, on the timeline.
Each row represents one group, and Horilla displays a count next to the group label.
If no grouping field is configured or resolved, all records are displayed together in a single row called “All“.
Timeline scale
The timeline can be displayed using different scales:
- Days
- Weeks
- Months
- Quarters
The scale is controlled through the timeline_scale query parameter.
The default scale is:
months
So the timeline will normally use months as its main date axis.
Horilla also automatically determines a suitable visible date range based on the records being displayed. When records are available, the range is fitted around their start and end dates with some additional margin.
If there are no records, it uses a default window extending approximately 90 days into the past and 30 days into the future.
7. One Important Detail: The Timeline URL
There is one behavior of HorillaTimelineView that’s important when integrating it into an application.
A normal, non-HTMX GET request directly to the timeline URL doesn’t simply render the timeline page.
Instead, the view redirects the request to the main view with:
layout=timeline
and preserves the current query parameters.
This is intentional.
The timeline is designed to work inside the same parent shell that handles the other layouts, such as list, kanban, and group-by views.
Because of this, you should normally enter the timeline through the parent shell view instead of opening the timeline endpoint directly as a standalone page.
This approach allows different layouts to share the same navigation and page structure.
8. Adding the Timeline to the Parent View
Once the timeline view itself is ready, you need to connect it to the parent view.
Suppose the opportunity module already has URLs for list, kanban, and group-by views.
You can add the timeline URL alongside them:
class OpportunityView(LoginRequiredMixin, HorillaView):
nav_url = reverse_lazy(“opportunities:opportunities_nav”)
list_url = reverse_lazy(“opportunities:opportunities_list”)
kanban_url = reverse_lazy(“opportunities:opportunities_kanban”)
group_by_url = reverse_lazy(“opportunities:opportunities_group_by”)
timeline_url = reverse_lazy(“opportunities:opportunities_timeline”)
The important part is:
timeline_url = reverse_lazy(“opportunities:opportunities_timeline”)
Now the parent shell knows where to find the timeline layout.
9. Registering the Timeline URL
The next step is to add the timeline endpoint to urls.py.
For example:
path(
“opportunities-timeline/”,
views.OpportunityTimelineView.as_view(),
name=”opportunities_timeline”,
),
At this point, the pieces are connected:
Parent View
↓
timeline_url
↓
OpportunityTimelineView
↓
HorillaTimelineView
↓
Timeline layout
You don’t need separate endpoints for expanding rows or loading more records.
The timeline reuses the pagination and HTMX infrastructure provided by HorillaListView.
The timeline’s default pagination is 200 records per page of the underlying queryset. This is pagination of the records themselves, rather than pagination separately inside each timeline row.
10. Filtering the Queryset for a Specific Timeline
Since HorillaTimelineView ultimately builds on HorillaListView, you can override get_queryset() just like you would in a regular list view.
This becomes useful when different versions of the timeline need to show different subsets of records.
For example, the lead timeline can show only non-converted leads by default, while still supporting a converted-lead view.
class LeadTimelineView(LoginRequiredMixin, HorillaTimelineView):
model = Lead
view_id = "leads-timeline"
filterset_class = LeadFilter
search_url = reverse_lazy("leads:leads_list")
main_url = reverse_lazy("leads:leads_view")
enable_quick_filters = True
timeline_start_field = "created_at"
timeline_end_field = "updated_at"
timeline_group_by_field = "lead_status"
timeline_title_field = "title"
columns = ["title", "first_name", "email", "lead_status"]
actions = LeadListView.actions
col_attrs = LeadListView.col_attrs
def get_queryset(self):
queryset = super().get_queryset()
view_type = self.request.GET.get("view_type") or self.get_default_view_type()
if view_type == "converted_lead":
queryset = queryset.filter(is_convert=True)
self.actions = None
else:
queryset = queryset.filter(is_convert=False)
return queryset
The important thing here is that the timeline doesn’t force you into a special way of querying records.
You can still use Django’s normal queryset logic.
In this example:
- Converted leads are filtered using is_convert=True.
- Converted leads have their actions disabled.
- The default timeline only shows non-converted leads.
This is one of the benefits of building the timeline on top of the existing list-view architecture.
11. Reusing Existing List View Configuration
You may have already spent time configuring your list view with actions, column attributes, permissions, and other settings.
The good news is that you don’t necessarily need to duplicate that work for the timeline.
For example:
actions = OpportunityListView.actions
col_attrs = OpportunityListView.col_attrs
This lets the timeline reuse the existing configuration.
The same idea applies to other list-view functionality inherited through HorillaListView.
So instead of treating the timeline as an entirely different feature, it’s better to think of it as another way of presenting the same underlying records.
The data and querying logic remain familiar; only the visual representation changes.
12. A Simple Timeline Configuration
For many use cases, the timeline-specific configuration can be very small.
You might only need these four properties:
timeline_start_field = "created_at"
timeline_end_field = "close_date"
timeline_group_by_field = "stage"
timeline_title_field = "name"
These four settings answer the main questions the timeline needs to know:
- When does the record start?
- When does it end?
- Which row should it appear in?
- What should be displayed on the bar?
Everything else can continue to come from the existing list-view infrastructure.
Adding a timeline view in Horilla doesn’t mean creating an entirely new system for displaying records.
HorillaTimelineView follows the same design philosophy as HorillaGroupByView: it extends the existing list-view infrastructure instead of replacing it.
That means your existing columns, filters, permissions, actions, queryset handling, and col_attrs can continue to work with the timeline.
The main configuration you usually need is simply:
timeline_start_field = “created_at”
timeline_end_field = “close_date”
timeline_group_by_field = “stage”
timeline_title_field = “name”
From there, connect the timeline_url to the parent shell, register the URL pattern, and you’re ready to use the timeline layout.
The result is a much more useful way to look at records where duration and timing matter. Instead of making users compare dates manually in a table, the timeline turns those dates into something they can understand at a glance.
And because it builds on top of HorillaListView, you get that visual timeline without having to throw away the infrastructure you’ve already built.