Skip to content
CanopyX

Resource timeline

Resources as a tree with capacity per group, events on a scrolling time axis, and rescheduling by drag.

CanopyCalendarResources723 events across a host treeDocumentation for the resource timeline

3 demos

Incidents by host Open this demo full screen

Resources as a tree with capacity per group, events on a scrolling time axis, and rescheduling by drag.

Build one like it

The smallest complete version, in each language CanopyX supports for this view. The demo above is the same API with more data.

using CanopyX;
using CanopyX.Html;

var hosts = new CanopyTreeView("hosts", "Hosts");
hosts.Columns.Add(new CanopyColumn("name", "Host"));
hosts.Columns.Add(new CanopyColumn("events", "Events") {
    Kind = CanopyColumnKind.Number
});
foreach (var (site, names) in new[] {
    ("Warsaw", new[] { "waw-web-01", "waw-db-01" }),
    ("Berlin", new[] { "ber-web-01", "ber-db-01" })
}) {
    var node = new CanopyNode("site-" + site.ToLowerInvariant(), site) {
        Expanded = true
    };
    foreach (var name in names) node.Children.Add(new CanopyNode(name, name));
    hosts.Roots.Add(node);
}

var events = new CanopyDataGridView("host-events", "Incidents by host") {
    Noun = "events"
};
events.Columns.Add(new CanopyColumn("title", "Event") { RowHeader = true });
events.Columns.Add(new CanopyColumn("kind", "Kind") {
    Presenter = CanopyGridPresenter.State(new Dictionary<string, CanopyTone> {
        ["Outage"] = CanopyTone.Danger,
        ["Incident"] = CanopyTone.Warning,
        ["Maintenance"] = CanopyTone.Info
    })
});
events.Columns.Add(new CanopyColumn("starts", "Starts") {
    Kind = CanopyColumnKind.DateTime
});
events.Columns.Add(new CanopyColumn("ends", "Ends") {
    Kind = CanopyColumnKind.DateTime
});
events.Columns.Add(new CanopyColumn("host", "Host"));
foreach (var (id, title, kind, host, hour, hours) in new[] {
    ("e1", "Patch window", "Maintenance", "waw-web-01", 6, 3),
    ("e2", "Replication lag", "Incident", "waw-db-01", 9, 2),
    ("e3", "Power feed lost", "Outage", "ber-web-01", 11, 1),
    ("e4", "Index rebuild", "Maintenance", "ber-db-01", 14, 4),
    ("e5", "Disk alerts", "Incident", "waw-db-01", 16, 2)
}) {
    var starts = new DateTime(2026, 10, 16, hour, 0, 0, DateTimeKind.Utc);
    var row = new CanopyGridRow(id);
    row.Cells["title"] = title;
    row.Cells["kind"] = kind;
    row.Cells["starts"] = starts;
    row.Cells["ends"] = starts.AddHours(hours);
    row.Cells["host"] = host;
    events.Rows.Add(row);
}

events.Calendar = new CanopyCalendar("starts") {
    End = "ends",
    Title = "title",
    Status = "kind",
    Resource = "host",
    View = CanopyCalendarView.Timeline,
    Zoom = CanopyCalendarZoom.Hour,
    Date = new DateTime(2026, 10, 16),
    Resources = new CanopyCalendarResources(hosts) { Label = "Hosts" }
};
events.Calendar.Resources.Aggregates["events"] = CanopyCalendarAggregate.Count;
events.SaveHtmlPage("timeline.html");

Run this snippet (resource timeline, C#)Its output, generated with the site.

Try this in the demo

  1. Resources as a tree

    Collapse a site, then a role, in the tree on the left.

    Each resource shows its event count and busy hours; group rows roll them up.

  2. Capacity per group

    Look along the group rows for amber and red cells.

    Group rows turn amber, then red, as more events run at the same time.

  3. Reschedule by drag

    Drag an incident onto another host in the same role.

    Moves go through the grid's editing contract; a move that would overlap is refused.