# ResourceTimeline
**Also known as:** ResourceScheduler, Gantt, ResourcePlanner
## Description
BitResourceTimeline is a high-performance resource timeline - a planner or scheduler with the time along the horizontal axis, the resources down the vertical one and the allocations drawn as bars in between, for gate and fleet planning, crew rosters, machine schedules and the like. One engine lays the board out and drives it while a canvas, SVG or HTML renderer paints it, so tens of thousands of bars stay fluid. It zooms from weeks down to minutes, selects and navigates with the pointer and the keyboard, edits (move, resize, create, copy, paste and delete) under the app's control, nests resources into collapsible groups, draws its axes in any time zone and locale, streams large data sets or loads them window by window on demand, and takes templates for its resource column, its tooltips and its corner.
## Parameters
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Classes | `BitResourceTimelineClassStyles?` | null | Custom CSS classes for different parts of the BitResourceTimeline. |
| Config | `BitResourceTimelineConfig?` | null | The resources, the time range and the bars to render. Compared by reference: a Config changed in place needs a ReloadAsync. |
| LoadAllocationsAsync | `Func<BitResourceTimelineWindow, Task<IReadOnlyList<BitResourceTimelineAllocation>>>?` | null | On-demand loading: called with the window the view needs (the visible range plus a buffer) for every allocation overlapping it, and again as a scroll or zoom nears its edge. Calls can overlap - a newer one does not wait for an older one, whose result is dropped. Swapping one delegate for another reloads nothing until the next window is fetched (ReloadAsync fetches it at once). The Config still supplies the resources and the range. Null loads everything up front. |
| LoadBatchSize | `int` | 10000 | Allocations per interop call; a larger set is streamed in batches of this size. Zero or less sends everything at once. |
| LoadingContent | `RenderFragment?` | null | Custom content of the loading overlay (a spinner and a text by default). |
| LoadingMinDurationMs | `int` | 0 | Minimum time in ms the loading overlay stays visible. |
| OnAllocationChanged | `EventCallback<BitResourceTimelineAllocation>` | | Raised after a bar is moved, resized or created, with the Config's own instance already updated - a handler only persists it. |
| OnAllocationChanging | `Func<BitResourceTimelineAllocationChange, Task<bool>>?` | null | Called before a single-bar move or resize is committed, with the bar already previewed; false restores it. Unset, the edit goes to OnAllocationsChanging (accepted when neither is set). |
| OnAllocationCreating | `Func<BitResourceTimelineCreateRequest, Task<BitResourceTimelineAllocation?>>?` | null | Called when a bar was drawn on empty content (EmptyDragAction.Create); returns the allocation with an Id of its own, or null to cancel. |
| OnAllocationsChanging | `Func<IReadOnlyList<BitResourceTimelineAllocationChange>, Task<bool>>?` | null | Called before a multi-bar commit (and a single-bar one while OnAllocationChanging is unset) with a change per bar; false restores them all. |
| OnAllocationsCopying | `Func<BitResourceTimelineCopyRequest, Task<IReadOnlyList<BitResourceTimelineAllocation>?>>?` | null | Called on paste (Ctrl/Cmd + V); returns the new allocations (with new ids), or null or empty to cancel. |
| OnAllocationsDeleting | `Func<IReadOnlyList<string>, Task<bool>>?` | null | Called with the ids of the bars Delete/Backspace is about to remove (Options.AllowDelete); false keeps them. |
| OnClick | `EventCallback<BitResourceTimelinePointerArgs>` | | Raised on a click or tap: the bar, the +N marker's bars, the resource row and the time under the pointer, its surface and viewport coordinates and the modifier keys. |
| OnContextMenu | `EventCallback<BitResourceTimelineContextMenuArgs>` | | Raised on a right-click of a bar, empty content or a resource row, with the arguments of OnClick (the browser's menu is suppressed). |
| OnDoubleClick | `EventCallback<BitResourceTimelinePointerArgs>` | | Raised on the second click of a double-click (or a double-tap), with the same arguments as OnClick, in addition to it. |
| OnResourceAxisWidthChanged | `EventCallback<int>` | | Raised with the new, clamped width once the user finishes resizing the resource column; pass it back as Options.ResourceAxisWidth to keep it. |
| OnSelectionChanged | `EventCallback<BitResourceTimelineAllocation[]>` | | Raised when the set of selected bars actually changes, with every selected bar in selection order (empty when nothing is selected) - the very instances of the Config. |
| OnViewChanged | `EventCallback<BitResourceTimelineView>` | | Raised at most once a frame when the visible span or the scale changes, for the chrome around the timeline. |
| Options | `BitResourceTimelineOptions?` | null | Dimensions, fonts, colors, time zone, zoom limits, editing and surface. A new instance is merged in: a property left null keeps the value in force (a default, or the theme's for colors and fonts). |
| ResourceTemplate | `RenderFragment<BitResourceTimelineRowContext>?` | null | Template of each row of the resource column, rendered in an overlay instead of the drawn labels; group rows get an expand/collapse button. |
| Styles | `BitResourceTimelineClassStyles?` | null | Custom CSS styles for different parts of the BitResourceTimeline. |
| Texts | `BitResourceTimelineTexts?` | null | The texts of the timeline: its accessible names, loading text, tooltip formats and announcements (English by default). |
| TooltipTemplate | `RenderFragment<BitResourceTimelineAllocation>?` | null | Template of the hover tooltip, rendered in a positioned overlay instead of the built-in one (a +N marker keeps the built-in one). Needs Options.ShowTooltips. |
| TopStartContent | `RenderFragment?` | null | Custom content of the top-start corner, where the two axes meet. |
| AriaLabel | `string?` | null | Gets or sets the accessible label for the component, used by assistive technologies. |
| Class | `string?` | null | Gets or sets the CSS class name(s) to apply to the rendered element. |
| Dir | `BitDir?` | null | Gets or sets the text directionality for the component's content. |
| Disabled | `bool` | false | Gets or sets a value indicating whether the component is disabled and cannot respond to user interaction. |
| ForceAnimation | `bool` | false | Gets or sets a value indicating whether the component's animations play at their full duration even when reduced motion is requested. |
| HtmlAttributes | `Dictionary<string, object>` | new Dictionary<string, object>() | Captures additional HTML attributes to be applied to the rendered element, in addition to the component's parameters. An attribute written on the component wins over an entry of the same name. |
| Id | `string?` | null | Gets or sets the unique identifier for the component's root element. |
| Style | `string?` | null | Gets or sets the CSS style string to apply to the rendered element. |
| TabIndex | `string?` | null | Gets or sets the tab order index for the component when navigating with the keyboard. |
| Visibility | `BitVisibility` | BitVisibility.Visible | Gets or sets the visibility state (visible, hidden, or collapsed) of the component. |
## Public Members
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| ClearSelectionAsync | `Func<Task>` | | Clears the selection of bars. |
| GetPixelsPerHourAsync | `Func<Task<double>>` | | Returns the current horizontal scale in pixels per hour (0 while the timeline is not ready). |
| GetSelectedBarsAsync | `Func<Task<IReadOnlyList<BitResourceTimelineAllocation>>>` | | Returns the selected bars in selection order; empty when nothing is selected. |
| GoToTodayAsync | `Func<Task<bool>>` | | Centers the current time; false when it is outside the time range. |
| PanByDaysAsync | `Func<int, bool?, Task<bool>>` | | Pans by whole days (negative back, 7 a week): exactly 24 hours, or to a local midnight with Options.PanToDayStart (panToDayStart overrides it). False when already at that end. |
| ReloadAsync | `Func<Task>` | | Sends the data again in full, after the Config was changed in place. |
| RemoveAllocationsAsync | `Func<IReadOnlyList<string>, Task>` | | Removes bars by id from the timeline, the selection and the focus; unknown ids are ignored. Config.Allocations is not changed. |
| ResetZoomAsync | `Func<Task<double>>` | | Returns to the automatic (or configured) scale, and returns the new scale in pixels per hour. |
| ScrollToAllocationAsync | `Func<string, Task<bool>>` | | Scrolls the bar's start and row into view; false for an unknown id. |
| ScrollToResourceAsync | `Func<string, Task<bool>>` | | Scrolls the row into view vertically; false for an unknown id. |
| ScrollToTimeAsync | `Func<DateTimeOffset, Task<bool>>` | | Centers the given time; false when it is outside the time range. |
| SelectAsync | `Func<IReadOnlyList<string>, bool, Task>` | | Sets the selection to the given allocation ids, or adds them to it when additive is true. Unknown ids are ignored. |
| SetPixelsPerHourAsync | `Func<double?, Task<double>>` | | Sets the scale in px per hour (clamped), or null for the automatic one; returns the new scale. |
| UpsertAllocationsAsync | `Func<IReadOnlyList<BitResourceTimelineAllocation>, Task>` | | Merges bars into the timeline by id (the last wins), keeping the selection and the focus. Config.Allocations is not changed. |
| ZoomInAsync | `Func<Task<double>>` | | Zooms in around the center; returns the new scale (0 while not ready). |
| ZoomOutAsync | `Func<Task<double>>` | | Zooms out around the center; returns the new scale (0 while not ready). |
| ZoomToDaysAsync | `Func<double, Task<double>>` | | Fits the given number of days into the content area around its center (clamped); returns the new scale. |
| UniqueId | `Guid` | Guid.NewGuid() | Gets the readonly unique identifier for the component's root element, assigned when the component instance is constructed. |
| RootElement | `ElementReference` | | Gets the reference to the root HTML element associated with this component. |
## Enums
### BitResourceTimelineAllocationChangeKind Enum
| Name | Value | Description |
| :--- | :--- | :---------- |
| Move | 0 | The bar was moved in time and/or onto another resource. |
| Resize | 1 | The start or the end of the bar was resized. |
| Create | 2 | A new bar is being created. Not handed to the changing callbacks today: a bar drawn on empty content is created through OnAllocationCreating alone. |
### BitResourceTimelineBarIconPosition Enum
| Name | Value | Description |
| :--- | :--- | :---------- |
| Start | 0 | Just before the start edge of the bar, vertically centered on it. |
| End | 1 | Just after the end edge of the bar, vertically centered on it. |
| Above | 2 | Centered above the bar. |
| Below | 3 | Centered below the bar. |
| Center | 4 | On top of the bar, centered horizontally and vertically within it. |
### BitResourceTimelineBarSectionPosition Enum
| Name | Value | Description |
| :--- | :--- | :---------- |
| Start | 0 | Just inside the start edge of the section. |
| Center | 1 | Centered horizontally within the section. |
| End | 2 | Just inside the end edge of the section. |
### BitResourceTimelineEmptyDragAction Enum
| Name | Value | Description |
| :--- | :--- | :---------- |
| Marquee | 0 | A rubber-band selection (the default). |
| Create | 1 | Draws a new allocation. Requires Options.Editable and an OnAllocationCreating handler; Ctrl/Cmd + drag still marquees. |
### BitResourceTimelineHitArea Enum
| Name | Value | Description |
| :--- | :--- | :---------- |
| Content | 0 | The scrollable grid of resource rows and bars, beside the resource column and below the time axis. |
| ResourceAxis | 1 | The sticky resource column. Has a resource but no time. |
| TimeAxis | 2 | The sticky time axis along the top. Has a time but no resource. |
| Corner | 3 | The top-start cell where the time axis and the resource column meet. Neither a time nor a resource. |
### BitResourceTimelineRendererType Enum
| Name | Value | Description |
| :--- | :--- | :---------- |
| Canvas | 0 | An HTML canvas (the default): immediate-mode 2D drawing, crisp on HiDPI screens, and the fastest option for large, dense data sets. |
| Svg | 1 | Inline SVG: resolution-independent vector output that is styleable, inspectable in the developer tools and copy and print friendly. Slower than the canvas on very dense scenes. |
| Html | 2 | Plain HTML elements: each visible bar is a real element carrying its allocation id in data-bar-id (and its ClassName), so it can be styled with CSS. Slower than the canvas on very dense scenes. |
### BitResourceTimelineStripeAlign Enum
| Name | Value | Description |
| :--- | :--- | :---------- |
| Continuous | 0 | The cycle runs across the whole timeline: adjacent days (or hours, or weeks) take the next color, without restarting at midnight or at the start of a week. The default. |
| Repeat | 1 | The cycle restarts each day when the unit is an hour, and each week when it is a day, so the same hour of the day or weekday always gets the same color. A week unit has no larger cycle and is painted as Continuous. |
### BitResourceTimelineSurfaceCombine Enum
| Name | Value | Description |
| :--- | :--- | :---------- |
| Overlay | 0 | The row fills, then the column stripes on top: a translucent column color tints the row under it, an opaque one covers it. The default. |
| RowsOnTop | 1 | The column stripes, then the row fills on top. |
| Checker | 2 | Each cell is painted from the column colors by its column index plus its row index, so two colors make a checkerboard over the whole surface; a resource's own Background still paints its row solid and the row stripes are not used. Cells that would be too thin or too many fall back to full-height column stripes. |
### BitResourceTimelineSurfaceUnit Enum
| Name | Value | Description |
| :--- | :--- | :---------- |
| Day | 0 | A stripe per local calendar day. The default. |
| Hour | 1 | A stripe per clock hour, continuing through midnight. |
| Week | 2 | A stripe per week, starting on Options.FirstDayOfWeek (or the week start of the locale, otherwise Monday). |
### BitVisibility Enum
| Name | Value | Description |
| :--- | :--- | :---------- |
| Visible | 0 | The content of the component is visible. |
| Hidden | 1 | The content of the component is hidden, but the space it takes on the page remains (visibility:hidden). |
| Collapsed | 2 | The component is hidden (display:none). |
### BitDir Enum
| Name | Value | Description |
| :--- | :--- | :---------- |
| Ltr | 0 | Ltr (left to right) is to be used for languages that are written from the left to the right (like English). |
| Rtl | 1 | Rtl (right to left) is to be used for languages that are written from the right to the left (like Arabic). |
| Auto | 2 | Auto lets the user agent decide. It uses a basic algorithm as it parses the characters inside the element until it finds a character with a strong directionality, then applies that directionality to the whole element. |
## Sub Classes
### BitResourceTimelineConfig Properties
Everything the timeline renders: the resource rows down the vertical axis, the time range along the horizontal axis and the allocation bars within it. Assign a new instance to the Config parameter to render it.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Resources | `List<BitResourceTimelineResource>` | | The resource rows rendered along the vertical axis. Required. |
| StartDate | `DateTimeOffset` | | The start of the time range rendered along the horizontal axis. Required. |
| EndDate | `DateTimeOffset` | | The end of the time range rendered along the horizontal axis. Required. |
| Allocations | `List<BitResourceTimelineAllocation>` | | The allocation bars to display (not read while LoadAllocationsAsync is set). Required. |
### BitResourceTimelineResource Properties
A row of the timeline (a gate, a vehicle, a machine, a person). Resources nest into a hierarchy of any depth through ParentId: a resource with children renders as a collapsible group row, and can still own allocations of its own.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Id | `string` | | The stable identifier the allocations are associated with. Required. |
| Name | `string` | | The label of the row on the resource axis. Required. |
| ParentId | `string?` | null | The Id of the parent resource. When set (and the parent exists), this resource is nested under it, forming a collapsible group; root resources leave it null. Sibling and root order follows the order of the list. |
| Collapsed | `bool` | false | The initial state of a resource that has children: when true, its descendants start hidden until the group is expanded. Ignored for a resource without children. |
| Background | `string?` | null | CSS color painted behind the row of this resource, under the grid and the bars. Wins over the row stripes and the ResourceColors of Options.Surface. |
| LineColor | `string?` | null | CSS color of the horizontal line along the top of the row of this resource. Wins over the row lines and the ResourceColors of Options.AxisLines. |
### BitResourceTimelineAllocation Properties
A single allocation period drawn as a bar on a resource row.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Id | `string` | | The unique identifier of the bar. Required. |
| ResourceId | `string` | | The Id of the resource the bar is drawn on. Required. |
| StartTime | `DateTimeOffset` | | The start of the period. |
| EndTime | `DateTimeOffset` | | The end of the period. |
| Color | `string?` | null | CSS color of the bar fill (for example #e8590c or tomato). Falls back to the bar color of the theme, or Options.Colors.Bar. |
| Height | `int?` | null | The height of the bar in pixels, vertically centered in its row. Falls back to Options.BarHeight. The edge bars share it. |
| TextAbove | `string?` | null | A label centered above the bar. |
| TextBelow | `string?` | null | A label centered below the bar. |
| TextStart | `string?` | null | A label just before the start edge of the bar. |
| TextEnd | `string?` | null | A label just after the end edge of the bar. |
| Tooltip | `string?` | null | The hover text of the bar (requires Options.ShowTooltips), a line break separating its lines. When null, a default is built from the labels, the resource name and the time range. |
| StartBar | `BitResourceTimelineEdgeBar?` | null | A decorative bar drawn immediately before the start edge, in the row and at the height of the bar - commonly a delay before the planned allocation began. |
| EndBar | `BitResourceTimelineEdgeBar?` | null | A decorative bar drawn immediately after the end edge, in the row and at the height of the bar - commonly a delay after the planned allocation ended. |
| Icons | `List<BitResourceTimelineBarIcon>?` | null | Icons or small images drawn around (or within) the bar, each anchored to its own position; several at one position line up away from the bar in list order. |
| Sections | `List<BitResourceTimelineBarSection>?` | null | Parts of the bar drawn with colors, text and an icon of their own, bounded by times or percentages and always within the bar, in list order. |
| Data | `JsonElement?` | null | A payload of the app preserved on the instance; the engine never reads or paints it. |
| Locked | `bool` | false | When true, the bar can be selected and shows its tooltip but cannot be moved or resized, by the pointer or the keyboard. |
| ClassName | `string?` | null | A CSS class put on the element of the bar by the HTML renderer (canvas and SVG ignore it), for paint through the app's own CSS. Pointer events still go to the surface, so :hover does not fire on it. |
### BitResourceTimelineEdgeBar Properties
A decorative bar drawn immediately before (StartBar) or after (EndBar) a main bar, in its row and at its height, typically a delay. It is not selectable.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Duration | `TimeSpan` | | The length of the edge bar: a start bar extends back from the start of the main bar, an end bar forward from its end. A duration of zero or less is not drawn. |
| Color | `string?` | null | CSS color of the edge bar. Falls back to the bar color of the theme. |
### BitResourceTimelineBarIcon Properties
A small icon or image drawn next to (or within) a bar, scaled to fit a square box while keeping its aspect ratio. It is decorative and not selectable.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Source | `string` | | The image source: an absolute or app-relative URL, or a data URI. Required. |
| Position | `BitResourceTimelineBarIconPosition` | Start | Where the icon is anchored relative to the bar. |
| Inside | `bool` | false | Draws the icon within the bar, against the edge its Position names, instead of beside it (Center always draws inside). An icon larger than the bar overhangs it, so give such a bar a Height that fits. |
| Size | `int?` | null | The side of the square box the icon is fitted in, in pixels. Falls back to Options.BarIconSize. |
### BitResourceTimelineBarSection Properties
A part of a bar with colors, text and an icon of its own - the phases of a turnaround, the part of a job that has run. Each edge is a time or a percentage of the bar (a time wins), an edge given neither way is the bar's own, and a section always stays within its bar. A time-bounded section stays where it is when the bar is moved, a percentage-bounded one travels with it. Sections are decorative: the whole bar stays the hit target.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| StartTime | `DateTimeOffset?` | null | The start of the section; wins over StartPercent. Clamped to the bar. |
| EndTime | `DateTimeOffset?` | null | The end of the section; wins over EndPercent. Clamped to the bar. |
| StartPercent | `double?` | null | The start of the section as a percentage (0-100) of the duration of the bar, from its start. Used when StartTime is null. |
| EndPercent | `double?` | null | The end of the section as a percentage (0-100) of the duration of the bar, from its start. Used when EndTime is null. |
| BackgroundColor | `string?` | null | CSS color filling the section. When null the fill of the bar shows through. |
| BorderColor | `string?` | null | CSS color of a 1px border just inside the edges of the section. When null no border is drawn. |
| ForegroundColor | `string?` | null | CSS color of the Text. Falls back to the bar label color. |
| Text | `string?` | null | The text drawn within the section, in the bar-label font. Text that does not fit is left out, never cut off. |
| TextPosition | `BitResourceTimelineBarSectionPosition` | Center | Where the Text sits within the section. |
| Icon | `string?` | null | An icon drawn within the section: an image URL or a data URI. One sharing a position with the text sits beside it, nearer the edge of the section (before the text when both are centered); one that does not fit is not drawn. |
| IconPosition | `BitResourceTimelineBarSectionPosition` | Start | Where the Icon sits within the section. |
| IconSize | `int?` | null | The side of the square box the Icon is fitted in, in pixels. Falls back to Options.BarIconSize, and shrinks to the height of a shorter bar. |
### BitResourceTimelineOptions Properties
The visual configuration of the timeline (defaults in parentheses). A new instance is merged into what is in force: a property left null keeps the value applied before it.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| ResourceHeight | `int?` | null | Minimum row height in px (40). A row grows as overlapping bars stack. |
| TimeAxisHeight | `int?` | null | Height of the time axis in px (60). |
| ResourceAxisWidth | `int?` | null | Width of the resource column in px (150). |
| ResourceAxisResizable | `bool?` | null | Whether the divider of the resource column can be dragged or moved from the keyboard (true); see OnResourceAxisWidthChanged. |
| ResourceAxisMinWidth | `int?` | null | Minimum width of the resource column in px (80). |
| ResourceAxisMaxWidth | `int?` | null | Maximum width of the resource column in px; unset, the viewport less 100px. |
| DateRowHeight | `int?` | null | Height of the date row of the time axis in px (22). |
| BarHeight | `int?` | null | Height of a bar without a Height of its own in px (4). |
| BarMargin | `int?` | null | Gap between bars stacked for overlapping in time, in px (2). |
| StackLabelClearance | `bool?` | null | Whether stacked bars also keep room for their labels and icons (true). Independent of the zoom. |
| StackOnLabelCollision | `bool?` | null | Whether bars whose labels, icons or edge bars would collide stack too, not only overlapping times (true). |
| MinBarWidth | `int?` | null | Minimum drawn width of a bar in px (2). |
| MinBarWidthForLabels | `int?` | null | Minimum bar width for its labels and icons to be drawn, in px (24). |
| BarLabelFont | `string?` | null | CSS font of the bar labels (11px, theme font family). |
| BarLabelGap | `int?` | null | Gap between a bar and its labels and icons in px (3). |
| BarIconSize | `int?` | null | Default box side of a bar icon in px (16). |
| DateLabelFont | `string?` | null | CSS font of the day labels (12px, theme font family). |
| HourLabelFont | `string?` | null | CSS font of the hour labels (12px, theme font family). |
| ResourceLabelFont | `string?` | null | CSS font of the leaf resource names (13px, theme font family). |
| ResourceGroupFont | `string?` | null | CSS font of the group resource names (bold 13px, theme font family). |
| ResourceChevronFont | `string?` | null | CSS font of the group chevron (10px, theme font family). |
| ResourceChevronGap | `int?` | null | Gap between a group's chevron and its name in px (14). |
| DragThreshold | `int?` | null | Pointer movement in px before a press becomes a drag (4). |
| HitTolerance | `int?` | null | Extra px around a bar that still hit it (3). |
| TimeZone | `string?` | null | IANA zone the axis is drawn in (UTC, Europe/Berlin); the viewer's while unset, or empty to return to it. Only the axis moves. |
| ShowUtcTime | `bool?` | null | Adds a UTC hour row above the TimeZone's (false); raise TimeAxisHeight for room. |
| Locale | `string?` | null | BCP 47 locale of the labels, tooltips and announcements; the viewer's while unset, or empty to return to it. |
| FirstDayOfWeek | `DayOfWeek?` | null | Where week stripes start; unset, the week start of the Locale (or the viewer's), else Monday. |
| Hour12 | `bool?` | null | Labels the hours with a 12-hour clock (false). |
| NonWorkingDays | `int[]?` | null | Weekdays washed as non-working (0 = Sunday ... 6 = Saturday). Visual only. |
| WorkingHoursStart | `int?` | null | Start of the working hours in minutes from local midnight (540 = 09:00); hours outside are washed. Visual only. |
| WorkingHoursEnd | `int?` | null | End of the working hours in minutes from local midnight (1020 = 17:00). Visual only. |
| MaxStackLanes | `int?` | null | Maximum lanes a cluster of overlapping bars stacks into; 0 is unlimited (the default). The rest hide behind a +N marker. |
| AllowDelete | `bool?` | null | Lets Delete/Backspace remove the selected bars of an editable timeline, once OnAllocationsDeleting agrees (false). |
| PixelsPerHour | `double?` | null | Horizontal scale; unset fits one day into the viewport. Clamped to Min/MaxPixelsPerHour; a zoom overrides it until ResetZoomAsync. |
| MinPixelsPerHour | `double?` | null | Lower bound of the zoom in px per hour (0.25). |
| MaxPixelsPerHour | `double?` | null | Upper bound of the zoom in px per hour (1200). |
| AutoScrollToNow | `bool?` | null | Centers the current time as the first load is laid out (false). |
| PreserveScrollOnReload | `bool?` | null | Keeps a reload showing the same time and row instead of moving the view (false). |
| NowLineRefreshMs | `int?` | null | How often the now line repaints, in ms (60000); 0 stops it. |
| PanToDayStart | `bool?` | null | Makes PanByDaysAsync land on a local midnight rather than step exactly 24 hours (false). |
| Editable | `bool?` | null | Enables moving and resizing bars by pointer and by Alt + arrows (false). |
| EditSnapMinutes | `int?` | null | Step a move or resize snaps to, in minutes (15); 0 is continuous. |
| SnapToTimeZone | `bool?` | null | Snaps edits to wall-clock steps of the TimeZone (true) rather than the Unix epoch. |
| EditResizeHandlePx | `int?` | null | Grab zone at each end of a bar that resizes it, in px (6). |
| EditMinDurationMinutes | `int?` | null | Shortest duration a resize or a drawn bar can produce, in minutes (5). |
| AllowResourceChange | `bool?` | null | Whether a move may put a bar on another row (true). |
| AllowOverlap | `bool?` | null | Whether unlocked bars of a row may overlap in time (true); false refuses such an edit. |
| EmptyDragAction | `BitResourceTimelineEmptyDragAction?` | null | What a drag on empty content does while Editable: Marquee (the default) or Create (needs OnAllocationCreating). |
| ShowTooltips | `bool?` | null | Whether hovering a bar or a +N marker shows a tooltip (true). |
| TooltipDelayMs | `int?` | null | Hover delay before a tooltip appears, in ms (300). |
| WindowBufferFactor | `double?` | null | On-demand loading: viewport widths of extra time asked for on each side (1). |
| WindowRefetchThreshold | `double?` | null | On-demand loading: a new window is asked for within this many viewport widths of the loaded edge (0.25). |
| WindowDebounceMs | `int?` | null | On-demand loading: delay after the last scroll before the window is checked, in ms (150). |
| Renderer | `BitResourceTimelineRendererType?` | null | What paints the timeline: Canvas (the default), Svg or Html; switchable at runtime. |
| Colors | `BitResourceTimelineColors?` | null | Color overrides, winning over the theme and the --bit-ResourceTimeline-* variables. |
| Surface | `BitResourceTimelineSurface?` | null | Stripes, per-resource colors and bands under the grid. Replaced as a whole; empty clears it. |
| AxisLines | `BitResourceTimelineAxisLines?` | null | Colors of the time and resource lines. Replaced as a whole; empty clears it. |
### BitResourceTimelineColors Properties
Color overrides of the timeline, each a CSS color string. A color set here wins over the bit theme and the --bit-ResourceTimeline-* variables; one left null keeps the color in force, so a partial instance changes only what it sets.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| ContentBg | `string?` | null | Background of the scrollable content area. |
| AxisBg | `string?` | null | Background of the time and resource axes and of their corner. |
| AxisBorder | `string?` | null | Lines within and around the axes. |
| Tick | `string?` | null | Hour ticks of the time axis. |
| Label | `string?` | null | Hour labels of the time axis and the resource labels. |
| DateLabel | `string?` | null | Day labels of the time axis. |
| Grid | `string?` | null | Grid lines across the content area. |
| Bar | `string?` | null | Fill of a bar that has no Color of its own. |
| BarSelected | `string?` | null | Fill of a selected bar that has no Color of its own. |
| BarSelectedBorder | `string?` | null | Outline drawn around a selected bar. |
| BarLabel | `string?` | null | Labels around the bars (above, below, start and end). |
| Now | `string?` | null | The current-time line and its marker. |
| SelectionFill | `string?` | null | Fill of the marquee selection rectangle. |
| SelectionBorder | `string?` | null | Outline of the marquee selection rectangle. |
| Focus | `string?` | null | The dashed keyboard focus ring of the focused bar. |
| TooltipBg | `string?` | null | Background of the built-in tooltip. |
| TooltipText | `string?` | null | Text of the built-in tooltip. |
| NonWorking | `string?` | null | The wash over the non-working days and hours. |
### BitResourceTimelineSurface Properties
Colors the content pane under the grid and the bars, painted bottom to top: the content background, the row fills, the column stripes, the Bands, then the non-working wash. A pattern is worked out for the whole range and resource list, so a stripe stays on its day or row as the user scrolls. Visual only.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Columns | `BitResourceTimelineColumnStripes?` | null | Vertical time stripes; two colors on the day unit alternate the calendar days. |
| Rows | `BitResourceTimelineRowStripes?` | null | Horizontal stripes down the visible resource rows. |
| Combine | `BitResourceTimelineSurfaceCombine?` | null | How a row color and a column color share a cell. Null paints the columns over the rows. |
| ResourceColors | `Dictionary<string, string>?` | null | A CSS color per resource id, for the rows that set no Background of their own. Wins over Rows. |
| Bands | `List<BitResourceTimelineSurfaceBand>?` | null | Explicit rectangles painted above the patterns; later entries paint over earlier ones. |
| ShadeTimeAxis | `bool?` | null | Also paints the column colors on the time axis: the date row for day and week stripes, the hour row for hour stripes. The labels stay above the tint. |
| ShadeResourceAxis | `bool?` | null | Also paints the color of each row in the resource column, behind its label. |
### BitResourceTimelineColumnStripes Properties
Vertical stripes, one per time unit, cycling Colors - the columns of a Surface or the vertical lines of AxisLines. Two colors on the default unit (a day) alternate the calendar days, and a day keeps its color wherever the loaded window sits.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Unit | `BitResourceTimelineSurfaceUnit?` | null | What one stripe covers. Null is a day. |
| Align | `BitResourceTimelineStripeAlign?` | null | Whether the cycle runs continuously across the range or restarts each day or week. Null is continuous. |
| Span | `int?` | null | How many units one color covers before the next; null or below 1 is 1. An hour pattern of three colors with a span of 8 paints three 8-hour shifts. |
| Offset | `int?` | null | Added to the stripe index before the color is chosen, so the cycle can start on another color without reordering Colors. |
| Colors | `string[]?` | null | The CSS colors cycled in order. At least one is required; a single color paints every stripe the same. |
### BitResourceTimelineRowStripes Properties
Horizontal stripes, one per visible resource row, cycling Colors down the rows - the rows of a Surface or the horizontal lines of AxisLines. A collapsed group re-stripes the rows under it; scrolling does not.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Colors | `string[]?` | null | The CSS colors cycled in order; two colors are a zebra. At least one is required. |
| Span | `int?` | null | How many rows one color covers before the next; null or below 1 is 1. |
| Offset | `int?` | null | Added to the row index before the color is chosen. |
### BitResourceTimelineSurfaceBand Properties
An explicit rectangle on the content pane, painted above the column and row patterns: across every visible row, or along the row of one resource.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Start | `DateTimeOffset` | | The start of the band (inclusive). |
| End | `DateTimeOffset` | | The end of the band (exclusive). A band ending at or before its Start is not painted. |
| Color | `string?` | null | The CSS color. A null or empty color is not painted. |
| ResourceId | `string?` | null | When set, only the row of this resource is painted, while it is visible; null paints every visible row. |
### BitResourceTimelineAxisLines Properties
Colors the grid lines bounding the time columns and the resource rows. A line left without a color keeps Colors.Grid; the day separators of the time axis take the color of their midnight, and a row with a line color of its own draws it across the resource column too. Visual only.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Vertical | `BitResourceTimelineColumnStripes?` | null | The colors of the vertical lines, chosen like column stripes: every vertical grid line takes the color of the stripe it sits in. |
| Horizontal | `BitResourceTimelineRowStripes?` | null | The colors of the horizontal lines - the top edge of each resource row - cycled down the rows. |
| ResourceColors | `Dictionary<string, string>?` | null | A CSS color per resource id for the line of its row, for the resources that set no LineColor of their own. |
| BoundariesOnly | `bool?` | null | When true, only the line at the start of each vertical stripe (each midnight for days, the week boundary, every hour the span covers) is recolored; the others keep Colors.Grid. |
### BitResourceTimelineAllocationChange Properties
A move or a resize handed to OnAllocationChanging / OnAllocationsChanging before it is committed. Return false to restore the previous resource and times without a reload.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Allocation | `BitResourceTimelineAllocation` | | The allocation being changed - the very instance of the Config - already updated with the previewed resource and times. |
| PreviousResourceId | `string` | | The resource of the allocation before this edit. |
| PreviousStartTime | `DateTimeOffset` | | The start time before this edit. |
| PreviousEndTime | `DateTimeOffset` | | The end time before this edit. |
| Kind | `BitResourceTimelineAllocationChangeKind` | Move | Whether the edit is a move or a resize. |
### BitResourceTimelineCreateRequest Properties
A new bar drawn by a drag on empty content, handed to OnAllocationCreating. The app returns the full allocation with an Id of its own, or null to cancel.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| ResourceId | `string` | | The resource row the new bar is drawn on. |
| StartTime | `DateTimeOffset` | | The snapped start of the drawn range. |
| EndTime | `DateTimeOffset` | | The snapped end of the drawn range (at least Options.EditMinDurationMinutes after the start). |
### BitResourceTimelineCopyRequest Properties
The copied bars a paste (Ctrl/Cmd + V) asks the app to clone, handed to OnAllocationsCopying. The app assigns the new ids.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Allocations | `IReadOnlyList<BitResourceTimelineAllocation>` | | The bars on the clipboard, in selection order. |
| Offset | `TimeSpan` | | The suggested time shift of each clone (typically one edit-snap step). The app may place the copies elsewhere. |
| ResourceId | `string?` | null | The resource under the keyboard focus when pasting, or that of the first copied bar when nothing is focused. Null when the timeline has no rows. |
### BitResourceTimelinePointerArgs Properties
The arguments of OnClick and OnDoubleClick: what was under the pointer (a bar, the bars of a +N marker, a resource row, a time) and where the pointer was, in the surface and in the viewport.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Allocation | `BitResourceTimelineAllocation?` | null | The bar under the pointer - the very instance of the Config - or null when no bar was hit (empty content, an axis, or a +N marker). |
| OverflowAllocations | `IReadOnlyList<BitResourceTimelineAllocation>` | [] | The hidden bars when the pointer hit the +N marker of a cluster; empty otherwise. |
| Resource | `BitResourceTimelineResource?` | null | The resource row under the pointer; null on the time axis, the corner, or below the last row. |
| Time | `DateTimeOffset?` | null | The time at the horizontal position of the pointer; null on the resource axis and the corner, which have no time coordinate. |
| Area | `BitResourceTimelineHitArea` | Content | The region of the timeline the pointer was in. |
| X | `double` | 0 | Pointer X in the coordinates of the timeline surface (origin at its top-left, the sticky axes included), independent of the page scroll. |
| Y | `double` | 0 | Pointer Y in the coordinates of the timeline surface (origin at its top-left, the sticky axes included), independent of the page scroll. |
| ClientX | `double` | 0 | Pointer X in viewport coordinates, suited for a position: fixed overlay. |
| ClientY | `double` | 0 | Pointer Y in viewport coordinates, suited for a position: fixed overlay. |
| CtrlKey | `bool` | false | Whether Ctrl was held. |
| ShiftKey | `bool` | false | Whether Shift was held. |
| MetaKey | `bool` | false | Whether Meta (Cmd on macOS, the Windows key elsewhere) was held. |
| AltKey | `bool` | false | Whether Alt was held. |
### BitResourceTimelineContextMenuArgs Properties
The arguments of OnContextMenu: what was under the pointer of a right-click and where it was, so the app can show and place a menu of its own (the browser's menu is suppressed). It derives from BitResourceTimelinePointerArgs and carries the same members.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Allocation | `BitResourceTimelineAllocation?` | null | The bar under the pointer - the very instance of the Config - or null when no bar was hit (empty content, an axis, or a +N marker). |
| OverflowAllocations | `IReadOnlyList<BitResourceTimelineAllocation>` | [] | The hidden bars when the pointer hit the +N marker of a cluster; empty otherwise. |
| Resource | `BitResourceTimelineResource?` | null | The resource row under the pointer; null on the time axis, the corner, or below the last row. |
| Time | `DateTimeOffset?` | null | The time at the horizontal position of the pointer; null on the resource axis and the corner, which have no time coordinate. |
| Area | `BitResourceTimelineHitArea` | Content | The region of the timeline the pointer was in. |
| X | `double` | 0 | Pointer X in the coordinates of the timeline surface (origin at its top-left, the sticky axes included), independent of the page scroll. |
| Y | `double` | 0 | Pointer Y in the coordinates of the timeline surface (origin at its top-left, the sticky axes included), independent of the page scroll. |
| ClientX | `double` | 0 | Pointer X in viewport coordinates, suited for a position: fixed overlay. |
| ClientY | `double` | 0 | Pointer Y in viewport coordinates, suited for a position: fixed overlay. |
| CtrlKey | `bool` | false | Whether Ctrl was held. |
| ShiftKey | `bool` | false | Whether Shift was held. |
| MetaKey | `bool` | false | Whether Meta (Cmd on macOS, the Windows key elsewhere) was held. |
| AltKey | `bool` | false | Whether Alt was held. |
### BitResourceTimelineRowContext Properties
The context of the ResourceTemplate for one visible row, reflecting the current state of the hierarchy.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Resource | `BitResourceTimelineResource` | | The resource the row stands for. |
| Depth | `int` | 0 | The zero-based depth of the row in the resource hierarchy (0 for root rows). |
| HasChildren | `bool` | false | Whether the resource has children, i.e. the row is a group header. |
| Collapsed | `bool` | false | Whether the group is currently collapsed. |
### BitResourceTimelineView Properties
The time span visible in the content area and the scale in force, raised by OnViewChanged.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Start | `DateTimeOffset` | | The time at the left edge of the content area. |
| End | `DateTimeOffset` | | The time at the right edge of the content area. |
| PixelsPerHour | `double` | 0 | The horizontal scale, in pixels per hour. |
### BitResourceTimelineWindow Properties
The time window LoadAllocationsAsync is asked for, already buffered beyond the visible range. The app returns every allocation overlapping it, across all the resources.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Start | `DateTimeOffset` | | The inclusive start of the requested window. |
| End | `DateTimeOffset` | | The inclusive end of the requested window. |
### BitResourceTimelineClassStyles Properties
Custom CSS classes/styles for the different parts of the BitResourceTimeline.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Root | `string?` | null | The root element. |
| Viewport | `string?` | null | The focusable scroll viewport the renderer draws into. |
| Corner | `string?` | null | The top-start corner holding the TopStartContent. |
| ResourceOverlay | `string?` | null | The overlay rendering the ResourceTemplate. |
| ResourceRow | `string?` | null | Each row of that overlay. |
| ResourceContent | `string?` | null | The rendered ResourceTemplate of each row. |
| GroupToggle | `string?` | null | The expand/collapse button of a group row. |
| TooltipOverlay | `string?` | null | The overlay rendering the TooltipTemplate. |
| Loading | `string?` | null | The loading overlay. |
| Spinner | `string?` | null | The default spinner of the loading overlay. |
| LoadingText | `string?` | null | The default text of the loading overlay. |
### BitResourceTimelineTexts Properties
The texts of the BitResourceTimeline: its accessible name and role description, the loading text, the labels of its controls, the formats of its tooltips and everything it announces to screen readers. All of them default to English. A text ending in Format is a composite format string.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| AriaLabel | `string` | Resource timeline | Accessible name while AriaLabel is unset. |
| RoleDescription | `string` | Resource timeline | Role description of the viewport. |
| Loading | `string` | Loading… | Text of the default loading overlay. |
| ExpandGroupFormat | `string` | Expand {0} | Name of a collapsed group's button; {0} the resource. |
| CollapseGroupFormat | `string` | Collapse {0} | Name of an expanded group's button; {0} the resource. |
| ResizeResourceColumn | `string` | Resize resource column | Name of the resource column's divider. |
| BarLocked | `string` | Bar is locked | Edit key on a locked bar. |
| SelectionCleared | `string` | Selection cleared | Escape cleared the selection. |
| EmptyRowFormat | `string` | {0}, no allocations | Focus on an empty row; {0} the resource. |
| FocusedBarFormat | `string` | {0}: {1} to {2} | Focus on a bar; {0} the resource, {1} start, {2} end. |
| FocusedLabeledBarFormat | `string` | {0}: {1}, {2} to {3} | Focus on a labeled bar; {0} the resource, {1} the label, {2} start, {3} end. |
| SelectedBarFormat | `string` | {0}, selected | A selected focused bar; {0} its announcement. |
| ResizedFormat | `string` | Resized: {0} to {1} | Keyboard resize; {0} start, {1} end. |
| MovedFormat | `string` | Moved: {0} to {1} | Keyboard move; {0} start, {1} end. |
| MovedToResourceFormat | `string` | Moved to {0}: {1} to {2} | Keyboard move to another row; {0} the resource, {1} start, {2} end. |
| EditRefused | `string` | Edit refused | An edit refused. |
| EditRefusedOverlap | `string` | Edit refused: overlaps another allocation | An edit refused for overlapping. |
| GroupCollapsed | `string` | Collapsed group | A group collapsed. |
| GroupExpanded | `string` | Expanded group | A group expanded. |
| CopiedFormat | `string` | Copied {0} | Bars copied; {0} the count. |
| PastedFormat | `string` | Pasted {0} | Bars pasted; {0} the count. |
| DeletedFormat | `string` | Deleted {0} | Bars deleted; {0} the count. |
| DeleteRefused | `string` | Delete refused | A delete refused. |
| TimeRangeFormat | `string` | {0} – {1} | A time range (tooltips, drag readout); {0} start, {1} end. |
| HiddenAllocationFormat | `string` | {0} hidden allocation | A +N tooltip's first line for one bar; {0} the count. |
| HiddenAllocationsFormat | `string` | {0} hidden allocations | A +N tooltip's first line; {0} the count. |
| HiddenAllocationItemFormat | `string` | {0} · {1} | A line of a +N tooltip; {0} the label, {1} the range. |
| MoreHiddenAllocationsFormat | `string` | … and {0} more | A +N tooltip's last line; {0} the bars left out. |
## CSS Variables
Public CSS custom properties read off the component's root. Set one on `:root` to restyle every instance, on any ancestor to restyle the ones inside it, or on the `Style` of one instance to restyle it and every instance nested inside it, which inherits the value too - a parameter changes one alone, as does the real CSS property on its `Style` / `Styles` when that property does not inherit (`opacity`); an inherited one (`color`, `font-family`) reaches the nested instances that do not set it themselves.
| Name | Default Value | Description |
| :--- | :------------ | :---------- |
| `--bit-ResourceTimeline-content-background` | var(--bit-clr-bg-pri) | Background of the scrollable content. Options.Colors.ContentBg wins over it. |
| `--bit-ResourceTimeline-axis-background` | var(--bit-clr-bg-sec) | Background of the time and resource axes and of their corner. Options.Colors.AxisBg wins over it. |
| `--bit-ResourceTimeline-axis-border-color` | var(--bit-clr-brd-pri) | Lines within and around the axes. Options.Colors.AxisBorder wins over it. |
| `--bit-ResourceTimeline-tick-color` | var(--bit-clr-brd-pri) | Hour ticks of the time axis. Options.Colors.Tick wins over it. |
| `--bit-ResourceTimeline-label-color` | var(--bit-clr-fg-sec) | Hour and resource labels. Options.Colors.Label wins over it. |
| `--bit-ResourceTimeline-date-label-color` | var(--bit-clr-fg-pri) | Date labels of the time axis. Options.Colors.DateLabel wins over it. |
| `--bit-ResourceTimeline-grid-color` | var(--bit-clr-brd-sec) | Grid lines of the content. Options.Colors.Grid wins over it. |
| `--bit-ResourceTimeline-bar-color` | var(--bit-clr-pri) | Fill of a bar that has no Color of its own. Options.Colors.Bar wins over it. |
| `--bit-ResourceTimeline-bar-selected-color` | var(--bit-clr-pri-dark) | Fill of a selected bar that has no Color of its own. Options.Colors.BarSelected wins over it. |
| `--bit-ResourceTimeline-bar-selected-border-color` | var(--bit-clr-pri-dark-active) | Outline of a selected bar. Options.Colors.BarSelectedBorder wins over it. |
| `--bit-ResourceTimeline-bar-label-color` | var(--bit-clr-fg-sec) | Labels around the bars. Options.Colors.BarLabel wins over it. |
| `--bit-ResourceTimeline-now-color` | var(--bit-clr-err) | The current-time line. Options.Colors.Now wins over it. |
| `--bit-ResourceTimeline-selection-background` | var(--bit-clr-pri-tint) | Fill of the marquee rectangle. Options.Colors.SelectionFill wins over it. |
| `--bit-ResourceTimeline-selection-border-color` | var(--bit-clr-pri) | Outline of the marquee rectangle. Options.Colors.SelectionBorder wins over it. |
| `--bit-ResourceTimeline-focus-color` | var(--bit-clr-pri-focus) | Focus indicator of the bars, of the viewport and of the column divider. Options.Colors.Focus wins over it for the bars. |
| `--bit-ResourceTimeline-tooltip-background` | var(--bit-clr-tooltip-bg) | Fill of the built-in tooltip. Options.Colors.TooltipBg wins over it. |
| `--bit-ResourceTimeline-tooltip-color` | var(--bit-clr-tooltip-fg) | Text of the built-in tooltip. Options.Colors.TooltipText wins over it. |
| `--bit-ResourceTimeline-non-working-background` | var(--bit-clr-fg-pri) at 6% | Wash over the non-working days and hours. Options.Colors.NonWorking wins over it. |
| `--bit-ResourceTimeline-font-family` | var(--bit-tpg-font-family) | Family of the labels the engine draws, while no font option sets them. |
| `--bit-ResourceTimeline-resource-color` | var(--bit-clr-fg-pri) | Text of the rows of the ResourceTemplate. |
| `--bit-ResourceTimeline-resource-font-size` | var(--bit-tpg-fs-sm) | Text size of the rows of the ResourceTemplate. |
| `--bit-ResourceTimeline-toggle-color` | var(--bit-clr-fg-sec) | The expand/collapse button of a group row of the ResourceTemplate. |
| `--bit-ResourceTimeline-toggle-hover-color` | var(--bit-clr-pri) | That button under the pointer. |
| `--bit-ResourceTimeline-loading-background` | var(--bit-clr-bg-pri) at 70% | The veil of the loading overlay. |
| `--bit-ResourceTimeline-loading-color` | var(--bit-clr-fg-sec) | The text of the loading overlay. |
| `--bit-ResourceTimeline-spinner-size` | spacing(4.5) | The default spinner of the loading overlay. |
| `--bit-ResourceTimeline-spinner-color` | var(--bit-clr-pri) | The turning arc of that spinner. |
| `--bit-ResourceTimeline-spinner-track-color` | var(--bit-clr-brd-sec) | The track of that spinner. |
## Examples
\n**Basic**:
```razor
```
```csharp
private readonly BitResourceTimelineConfig basicConfig = CreateBasicConfig();
private static BitResourceTimelineConfig CreateBasicConfig()
{
var today = DateTime.Today;
// A local time of today (or of a day after it), converted with the offset of its own date, so a DST change in
// the range does not move the bars after it off their wall-clock times.
DateTimeOffset At(double hours, int days = 0) => new(today.AddDays(days).AddHours(hours));
return new()
{
StartDate = At(0),
EndDate = At(0, days: 3),
Resources =
[
new() { Id = "A1", Name = "Gate A1" },
new() { Id = "A2", Name = "Gate A2" },
new() { Id = "A3", Name = "Gate A3" },
new() { Id = "A4", Name = "Gate A4" },
],
Allocations =
[
new() { Id = "f1", ResourceId = "A1", StartTime = At(1), EndTime = At(3.5) },
new() { Id = "f2", ResourceId = "A1", StartTime = At(6), EndTime = At(8) },
new() { Id = "f3", ResourceId = "A1", StartTime = At(13), EndTime = At(16) },
new() { Id = "f4", ResourceId = "A2", StartTime = At(2), EndTime = At(4) },
new() { Id = "f5", ResourceId = "A2", StartTime = At(9.5), EndTime = At(12) },
new() { Id = "f6", ResourceId = "A3", StartTime = At(4), EndTime = At(9) },
new() { Id = "f7", ResourceId = "A3", StartTime = At(17), EndTime = At(20) },
new() { Id = "f8", ResourceId = "A4", StartTime = At(7), EndTime = At(10) },
new() { Id = "f9", ResourceId = "A4", StartTime = At(20), EndTime = At(3, days: 1) },
new() { Id = "f10", ResourceId = "A2", StartTime = At(8, days: 1), EndTime = At(11, days: 1) },
],
};
}
```
\n**Resource hierarchy**:
```razor
```
```csharp
private readonly BitResourceTimelineConfig hierarchyConfig = CreateHierarchyConfig();
private static BitResourceTimelineConfig CreateHierarchyConfig()
{
var today = BitResourceTimelineDemoData.Today;
List resources =
[
new() { Id = "TA", Name = "Terminal A" },
new() { Id = "A1", Name = "Gate A1", ParentId = "TA" },
new() { Id = "A2", Name = "Gate A2", ParentId = "TA" },
new() { Id = "AR", Name = "Remote stands", ParentId = "TA" },
new() { Id = "R1", Name = "Stand R1", ParentId = "AR" },
new() { Id = "R2", Name = "Stand R2", ParentId = "AR" },
new() { Id = "TB", Name = "Terminal B", Collapsed = true },
new() { Id = "B1", Name = "Gate B1", ParentId = "TB" },
new() { Id = "B2", Name = "Gate B2", ParentId = "TB" },
];
return new()
{
StartDate = today,
EndDate = today.AddDays(3),
Resources = resources,
Allocations =
[
// The flights of the gates and the stands; the group rows get none from the helper.
.. BitResourceTimelineDemoData.Flights(resources, today, today.AddDays(3)),
// A group row can still hold bars of its own.
new() { Id = "TA-cleaning", ResourceId = "TA", StartTime = today.AddHours(1), EndTime = today.AddHours(4) },
new() { Id = "TA-inspection", ResourceId = "TA", StartTime = today.AddHours(13), EndTime = today.AddHours(15) },
],
};
}
```
`BitResourceTimelineDemoData.cs`:
```csharp
using System.Globalization;
namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Extras.ResourceTimeline;
///
/// The sample data of the BitResourceTimeline demo: the gates of an airport and the turnarounds of the flights parked
/// at them, laid out around today so the current time is always on the timeline. Every day of every gate is generated
/// from a seed of its own, so the same arguments always return the same flights - and a window asked for on demand
/// returns exactly the flights a full load would have had there.
///
public static class BitResourceTimelineDemoData
{
private static readonly string[] Airlines = ["LH", "BA", "AF", "KL", "EK", "QR", "TK", "IB"];
public static readonly string WarningIcon = Svg("");
public static readonly string CheckIcon = Svg("");
public static readonly string StarIcon = Svg("");
public static readonly string LockIcon = Svg("");
public static readonly string PlaneIcon = Svg("");
/// The local midnight of today, which every example lays its bars out around.
public static DateTimeOffset Today => new(DateTime.Today);
///
/// The local midnight days from today, with the offset in force on that day - which a
/// daylight saving change in between makes differ from the one carries.
///
public static DateTimeOffset Day(int days) => new(DateTime.Today.AddDays(days));
/// Gates named Gate A1, Gate A2, ... - optionally the children of a group row.
public static List Gates(int count, string terminal = "A", string? parentId = null) =>
[.. Enumerable.Range(1, count).Select(n => new BitResourceTimelineResource { Id = $"{terminal}{n}", Name = $"Gate {terminal}{n}", ParentId = parentId })];
/// Terminal A, B, ... as group rows, each holding gates.
public static List Terminals(int terminals, int gatesPerTerminal)
{
var resources = new List();
for (var t = 0; t < terminals; t++)
{
var terminal = ((char)('A' + t)).ToString();
resources.Add(new() { Id = $"T{terminal}", Name = $"Terminal {terminal}" });
resources.AddRange(Gates(gatesPerTerminal, terminal, $"T{terminal}"));
}
return resources;
}
///
/// A whole board: the resources, the days from to around today,
/// and the flights of every gate in between.
///
public static BitResourceTimelineConfig Airport(List resources, int fromDay = -7, int toDay = 7, bool labeled = false)
{
// Today is read once, so a call running across midnight gives the range and the flights the same days.
var today = DateTime.Today;
var start = new DateTimeOffset(today.AddDays(fromDay));
var end = new DateTimeOffset(today.AddDays(toDay));
return new()
{
Resources = resources,
StartDate = start,
EndDate = end,
Allocations = Flights(resources, start, end, labeled),
};
}
///
/// The turnarounds overlapping .. at every resource that is not a group
/// row: 45 minutes to 3 hours each, 30 minutes to 2 hours apart. With each carries its
/// flight number above it.
///
public static List Flights(IReadOnlyList resources, DateTimeOffset from, DateTimeOffset to, bool labeled = false)
{
var groups = resources.Where(r => r.ParentId is not null).Select(r => r.ParentId).ToHashSet();
// Counted in local calendar days rather than elapsed 24 hours, which a daylight saving change in between would
// throw off by one: the day the window starts on, through the day it ends on unless it ends at its midnight.
// Today is read once, so a call running across midnight counts the days and lays them out from the same one.
var today = DateTime.Today;
var localTo = to.LocalDateTime;
var firstDay = (int)(from.LocalDateTime.Date - today).TotalDays;
var lastDay = (int)(localTo.Date - today).TotalDays + (localTo.TimeOfDay > TimeSpan.Zero ? 1 : 0);
var flights = new List();
for (var r = 0; r < resources.Count; r++)
{
var gate = resources[r];
if (groups.Contains(gate.Id)) continue;
for (var day = firstDay; day < lastDay; day++)
{
var dayStart = new DateTimeOffset(today.AddDays(day));
var dayEnd = new DateTimeOffset(today.AddDays(day + 1));
// Seeded by the gate and the calendar date, so a day reads the same whichever window asks for it, and
// whenever it is asked for (a fetch after midnight still finds the flights the earlier ones loaded).
var random = new Random(unchecked(r * 100_003 + dayStart.Year * 10_000 + dayStart.Month * 100 + dayStart.Day));
var start = dayStart.AddMinutes(random.Next(0, 12) * 15);
for (var n = 0; ; n++)
{
var end = start.AddMinutes(random.Next(3, 13) * 15);
var flightNumber = $"{Airlines[random.Next(Airlines.Length)]}{random.Next(100, 1000)}";
if (end > dayEnd) break;
if (end > from && start < to)
{
flights.Add(new()
{
Id = $"{gate.Id}-{dayStart.ToString("yyyyMMdd", CultureInfo.InvariantCulture)}-{n}",
ResourceId = gate.Id,
StartTime = start,
EndTime = end,
TextAbove = labeled ? flightNumber : null,
});
}
start = end.AddMinutes(random.Next(2, 9) * 15);
}
}
}
return flights;
}
// An inline SVG as a data URI, so the icons need no image files.
private static string Svg(string svg) => "data:image/svg+xml," + Uri.EscapeDataString(svg);
}
```
\n**Bar appearance**:
```razor
```
```csharp
private readonly BitResourceTimelineConfig appearanceConfig = CreateAppearanceConfig();
private static BitResourceTimelineConfig CreateAppearanceConfig()
{
var today = BitResourceTimelineDemoData.Today;
return new()
{
StartDate = today,
EndDate = today.AddDays(1),
Resources = BitResourceTimelineDemoData.Gates(4),
Allocations =
[
new()
{
Id = "LH441",
ResourceId = "A1",
StartTime = today.AddHours(2),
EndTime = today.AddHours(6),
Color = "#2f9e44",
Height = 10,
TextAbove = "LH441",
TextBelow = "Frankfurt",
TextStart = "02:00",
TextEnd = "06:00",
},
new()
{
Id = "BA117",
ResourceId = "A1",
StartTime = today.AddHours(11),
EndTime = today.AddHours(15),
Color = "#1971c2",
Height = 6,
TextAbove = "BA117 (hover me)",
Tooltip = "BA117 to London\nBoarding at 14:20, gate closes at 14:45",
},
new()
{
Id = "AF1380",
ResourceId = "A2",
StartTime = today.AddHours(4),
EndTime = today.AddHours(8),
Color = "#e8590c",
Height = 10,
TextAbove = "AF1380 (delayed)",
StartBar = new() { Duration = TimeSpan.FromMinutes(50), Color = "#ffa8a8" },
EndBar = new() { Duration = TimeSpan.FromMinutes(30), Color = "#fa5252" },
},
new()
{
Id = "KL1002",
ResourceId = "A3",
StartTime = today.AddHours(3),
EndTime = today.AddHours(8),
Color = "#9c36b5",
Height = 8,
Icons =
[
new() { Source = BitResourceTimelineDemoData.WarningIcon, Position = BitResourceTimelineBarIconPosition.Start },
new() { Source = BitResourceTimelineDemoData.CheckIcon, Position = BitResourceTimelineBarIconPosition.End },
new() { Source = BitResourceTimelineDemoData.StarIcon, Position = BitResourceTimelineBarIconPosition.Above },
new() { Source = BitResourceTimelineDemoData.StarIcon, Position = BitResourceTimelineBarIconPosition.Above },
],
},
new()
{
Id = "EK47",
ResourceId = "A4",
StartTime = today.AddHours(2),
EndTime = today.AddHours(9),
Color = "#0c8599",
Height = 22,
Icons =
[
new() { Source = BitResourceTimelineDemoData.PlaneIcon, Position = BitResourceTimelineBarIconPosition.Start, Inside = true },
new() { Source = BitResourceTimelineDemoData.LockIcon, Position = BitResourceTimelineBarIconPosition.Center },
new() { Source = BitResourceTimelineDemoData.CheckIcon, Position = BitResourceTimelineBarIconPosition.End, Inside = true, Size = 14 },
],
},
],
};
}
```
`BitResourceTimelineDemoData.cs`:
```csharp
using System.Globalization;
namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Extras.ResourceTimeline;
///
/// The sample data of the BitResourceTimeline demo: the gates of an airport and the turnarounds of the flights parked
/// at them, laid out around today so the current time is always on the timeline. Every day of every gate is generated
/// from a seed of its own, so the same arguments always return the same flights - and a window asked for on demand
/// returns exactly the flights a full load would have had there.
///
public static class BitResourceTimelineDemoData
{
private static readonly string[] Airlines = ["LH", "BA", "AF", "KL", "EK", "QR", "TK", "IB"];
public static readonly string WarningIcon = Svg("");
public static readonly string CheckIcon = Svg("");
public static readonly string StarIcon = Svg("");
public static readonly string LockIcon = Svg("");
public static readonly string PlaneIcon = Svg("");
/// The local midnight of today, which every example lays its bars out around.
public static DateTimeOffset Today => new(DateTime.Today);
///
/// The local midnight days from today, with the offset in force on that day - which a
/// daylight saving change in between makes differ from the one carries.
///
public static DateTimeOffset Day(int days) => new(DateTime.Today.AddDays(days));
/// Gates named Gate A1, Gate A2, ... - optionally the children of a group row.
public static List Gates(int count, string terminal = "A", string? parentId = null) =>
[.. Enumerable.Range(1, count).Select(n => new BitResourceTimelineResource { Id = $"{terminal}{n}", Name = $"Gate {terminal}{n}", ParentId = parentId })];
/// Terminal A, B, ... as group rows, each holding gates.
public static List Terminals(int terminals, int gatesPerTerminal)
{
var resources = new List();
for (var t = 0; t < terminals; t++)
{
var terminal = ((char)('A' + t)).ToString();
resources.Add(new() { Id = $"T{terminal}", Name = $"Terminal {terminal}" });
resources.AddRange(Gates(gatesPerTerminal, terminal, $"T{terminal}"));
}
return resources;
}
///
/// A whole board: the resources, the days from to around today,
/// and the flights of every gate in between.
///
public static BitResourceTimelineConfig Airport(List resources, int fromDay = -7, int toDay = 7, bool labeled = false)
{
// Today is read once, so a call running across midnight gives the range and the flights the same days.
var today = DateTime.Today;
var start = new DateTimeOffset(today.AddDays(fromDay));
var end = new DateTimeOffset(today.AddDays(toDay));
return new()
{
Resources = resources,
StartDate = start,
EndDate = end,
Allocations = Flights(resources, start, end, labeled),
};
}
///
/// The turnarounds overlapping .. at every resource that is not a group
/// row: 45 minutes to 3 hours each, 30 minutes to 2 hours apart. With each carries its
/// flight number above it.
///
public static List Flights(IReadOnlyList resources, DateTimeOffset from, DateTimeOffset to, bool labeled = false)
{
var groups = resources.Where(r => r.ParentId is not null).Select(r => r.ParentId).ToHashSet();
// Counted in local calendar days rather than elapsed 24 hours, which a daylight saving change in between would
// throw off by one: the day the window starts on, through the day it ends on unless it ends at its midnight.
// Today is read once, so a call running across midnight counts the days and lays them out from the same one.
var today = DateTime.Today;
var localTo = to.LocalDateTime;
var firstDay = (int)(from.LocalDateTime.Date - today).TotalDays;
var lastDay = (int)(localTo.Date - today).TotalDays + (localTo.TimeOfDay > TimeSpan.Zero ? 1 : 0);
var flights = new List();
for (var r = 0; r < resources.Count; r++)
{
var gate = resources[r];
if (groups.Contains(gate.Id)) continue;
for (var day = firstDay; day < lastDay; day++)
{
var dayStart = new DateTimeOffset(today.AddDays(day));
var dayEnd = new DateTimeOffset(today.AddDays(day + 1));
// Seeded by the gate and the calendar date, so a day reads the same whichever window asks for it, and
// whenever it is asked for (a fetch after midnight still finds the flights the earlier ones loaded).
var random = new Random(unchecked(r * 100_003 + dayStart.Year * 10_000 + dayStart.Month * 100 + dayStart.Day));
var start = dayStart.AddMinutes(random.Next(0, 12) * 15);
for (var n = 0; ; n++)
{
var end = start.AddMinutes(random.Next(3, 13) * 15);
var flightNumber = $"{Airlines[random.Next(Airlines.Length)]}{random.Next(100, 1000)}";
if (end > dayEnd) break;
if (end > from && start < to)
{
flights.Add(new()
{
Id = $"{gate.Id}-{dayStart.ToString("yyyyMMdd", CultureInfo.InvariantCulture)}-{n}",
ResourceId = gate.Id,
StartTime = start,
EndTime = end,
TextAbove = labeled ? flightNumber : null,
});
}
start = end.AddMinutes(random.Next(2, 9) * 15);
}
}
}
return flights;
}
// An inline SVG as a data URI, so the icons need no image files.
private static string Svg(string svg) => "data:image/svg+xml," + Uri.EscapeDataString(svg);
}
```
\n**Bar sections**:
```razor
```
```csharp
private readonly BitResourceTimelineConfig sectionsConfig = CreateSectionsConfig();
private static BitResourceTimelineConfig CreateSectionsConfig()
{
var today = BitResourceTimelineDemoData.Today;
return new()
{
StartDate = today,
EndDate = today.AddDays(1),
Resources = BitResourceTimelineDemoData.Gates(3),
Allocations =
[
new()
{
// Bounded by times: deboarding until 03:00, boarding from 06:30.
Id = "LH441",
ResourceId = "A1",
StartTime = today.AddHours(2),
EndTime = today.AddHours(8),
Color = "#1971c2",
Height = 18,
Sections =
[
new() { EndTime = today.AddHours(3), BackgroundColor = "#fab005", ForegroundColor = "#212529", Text = "Deboard" },
new()
{
StartTime = today.AddHours(3),
EndTime = today.AddHours(6.5),
ForegroundColor = "#ffffff",
Text = "LH441",
Icon = BitResourceTimelineDemoData.PlaneIcon,
IconSize = 14,
},
new()
{
StartTime = today.AddHours(6.5),
BackgroundColor = "#d0ebff",
BorderColor = "#1971c2",
ForegroundColor = "#1971c2",
Text = "Board",
TextPosition = BitResourceTimelineBarSectionPosition.End,
},
],
},
new()
{
// Bounded by a percentage: the part of the job that has run.
Id = "job",
ResourceId = "A2",
StartTime = today.AddHours(1),
EndTime = today.AddHours(9),
Color = "#ced4da",
Height = 18,
Sections =
[
new()
{
EndPercent = 40,
BackgroundColor = "#2f9e44",
ForegroundColor = "#ffffff",
Text = "40% done",
TextPosition = BitResourceTimelineBarSectionPosition.Start,
Icon = BitResourceTimelineDemoData.CheckIcon,
IconPosition = BitResourceTimelineBarSectionPosition.End,
},
],
},
new()
{
// Percentages travel and stretch with the bar when it is edited.
Id = "BA117",
ResourceId = "A3",
StartTime = today.AddHours(3),
EndTime = today.AddHours(10),
Color = "#9c36b5",
Height = 18,
Sections =
[
new() { EndPercent = 20, BackgroundColor = "#e599f7", ForegroundColor = "#212529", Text = "Prep" },
new()
{
StartPercent = 20,
EndPercent = 85,
ForegroundColor = "#ffffff",
Text = "BA117",
Icon = BitResourceTimelineDemoData.StarIcon,
IconPosition = BitResourceTimelineBarSectionPosition.Center,
},
new()
{
StartPercent = 85,
BackgroundColor = "#f8f0fc",
BorderColor = "#9c36b5",
ForegroundColor = "#9c36b5",
Text = "Wrap",
TextPosition = BitResourceTimelineBarSectionPosition.End,
},
],
},
],
};
}
```
`BitResourceTimelineDemoData.cs`:
```csharp
using System.Globalization;
namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Extras.ResourceTimeline;
///
/// The sample data of the BitResourceTimeline demo: the gates of an airport and the turnarounds of the flights parked
/// at them, laid out around today so the current time is always on the timeline. Every day of every gate is generated
/// from a seed of its own, so the same arguments always return the same flights - and a window asked for on demand
/// returns exactly the flights a full load would have had there.
///
public static class BitResourceTimelineDemoData
{
private static readonly string[] Airlines = ["LH", "BA", "AF", "KL", "EK", "QR", "TK", "IB"];
public static readonly string WarningIcon = Svg("");
public static readonly string CheckIcon = Svg("");
public static readonly string StarIcon = Svg("");
public static readonly string LockIcon = Svg("");
public static readonly string PlaneIcon = Svg("");
/// The local midnight of today, which every example lays its bars out around.
public static DateTimeOffset Today => new(DateTime.Today);
///
/// The local midnight days from today, with the offset in force on that day - which a
/// daylight saving change in between makes differ from the one carries.
///
public static DateTimeOffset Day(int days) => new(DateTime.Today.AddDays(days));
/// Gates named Gate A1, Gate A2, ... - optionally the children of a group row.
public static List Gates(int count, string terminal = "A", string? parentId = null) =>
[.. Enumerable.Range(1, count).Select(n => new BitResourceTimelineResource { Id = $"{terminal}{n}", Name = $"Gate {terminal}{n}", ParentId = parentId })];
/// Terminal A, B, ... as group rows, each holding gates.
public static List Terminals(int terminals, int gatesPerTerminal)
{
var resources = new List();
for (var t = 0; t < terminals; t++)
{
var terminal = ((char)('A' + t)).ToString();
resources.Add(new() { Id = $"T{terminal}", Name = $"Terminal {terminal}" });
resources.AddRange(Gates(gatesPerTerminal, terminal, $"T{terminal}"));
}
return resources;
}
///
/// A whole board: the resources, the days from to around today,
/// and the flights of every gate in between.
///
public static BitResourceTimelineConfig Airport(List resources, int fromDay = -7, int toDay = 7, bool labeled = false)
{
// Today is read once, so a call running across midnight gives the range and the flights the same days.
var today = DateTime.Today;
var start = new DateTimeOffset(today.AddDays(fromDay));
var end = new DateTimeOffset(today.AddDays(toDay));
return new()
{
Resources = resources,
StartDate = start,
EndDate = end,
Allocations = Flights(resources, start, end, labeled),
};
}
///
/// The turnarounds overlapping .. at every resource that is not a group
/// row: 45 minutes to 3 hours each, 30 minutes to 2 hours apart. With each carries its
/// flight number above it.
///
public static List Flights(IReadOnlyList resources, DateTimeOffset from, DateTimeOffset to, bool labeled = false)
{
var groups = resources.Where(r => r.ParentId is not null).Select(r => r.ParentId).ToHashSet();
// Counted in local calendar days rather than elapsed 24 hours, which a daylight saving change in between would
// throw off by one: the day the window starts on, through the day it ends on unless it ends at its midnight.
// Today is read once, so a call running across midnight counts the days and lays them out from the same one.
var today = DateTime.Today;
var localTo = to.LocalDateTime;
var firstDay = (int)(from.LocalDateTime.Date - today).TotalDays;
var lastDay = (int)(localTo.Date - today).TotalDays + (localTo.TimeOfDay > TimeSpan.Zero ? 1 : 0);
var flights = new List();
for (var r = 0; r < resources.Count; r++)
{
var gate = resources[r];
if (groups.Contains(gate.Id)) continue;
for (var day = firstDay; day < lastDay; day++)
{
var dayStart = new DateTimeOffset(today.AddDays(day));
var dayEnd = new DateTimeOffset(today.AddDays(day + 1));
// Seeded by the gate and the calendar date, so a day reads the same whichever window asks for it, and
// whenever it is asked for (a fetch after midnight still finds the flights the earlier ones loaded).
var random = new Random(unchecked(r * 100_003 + dayStart.Year * 10_000 + dayStart.Month * 100 + dayStart.Day));
var start = dayStart.AddMinutes(random.Next(0, 12) * 15);
for (var n = 0; ; n++)
{
var end = start.AddMinutes(random.Next(3, 13) * 15);
var flightNumber = $"{Airlines[random.Next(Airlines.Length)]}{random.Next(100, 1000)}";
if (end > dayEnd) break;
if (end > from && start < to)
{
flights.Add(new()
{
Id = $"{gate.Id}-{dayStart.ToString("yyyyMMdd", CultureInfo.InvariantCulture)}-{n}",
ResourceId = gate.Id,
StartTime = start,
EndTime = end,
TextAbove = labeled ? flightNumber : null,
});
}
start = end.AddMinutes(random.Next(2, 9) * 15);
}
}
}
return flights;
}
// An inline SVG as a data URI, so the icons need no image files.
private static string Svg(string svg) => "data:image/svg+xml," + Uri.EscapeDataString(svg);
}
```
\n**Renderers**:
```razor
rendererOptions = new() { Renderer = renderer }">
```
```csharp
private readonly BitResourceTimelineConfig rendererConfig = CreateRendererConfig();
private BitResourceTimelineRendererType renderer = BitResourceTimelineRendererType.Canvas;
private BitResourceTimelineOptions rendererOptions = new() { Renderer = BitResourceTimelineRendererType.Canvas };
private static BitResourceTimelineConfig CreateRendererConfig()
{
var config = BitResourceTimelineDemoData.Airport(BitResourceTimelineDemoData.Gates(6), fromDay: 0, toDay: 3, labeled: true);
// The HTML renderer puts ClassName on the element of the bar: the stylesheet hatches the flights already gone.
foreach (var flight in config.Allocations.Where(f => f.EndTime < DateTimeOffset.Now))
{
flight.ClassName = "departed";
}
return config;
}
```
`BitResourceTimelineDemoData.cs`:
```csharp
using System.Globalization;
namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Extras.ResourceTimeline;
///
/// The sample data of the BitResourceTimeline demo: the gates of an airport and the turnarounds of the flights parked
/// at them, laid out around today so the current time is always on the timeline. Every day of every gate is generated
/// from a seed of its own, so the same arguments always return the same flights - and a window asked for on demand
/// returns exactly the flights a full load would have had there.
///
public static class BitResourceTimelineDemoData
{
private static readonly string[] Airlines = ["LH", "BA", "AF", "KL", "EK", "QR", "TK", "IB"];
public static readonly string WarningIcon = Svg("");
public static readonly string CheckIcon = Svg("");
public static readonly string StarIcon = Svg("");
public static readonly string LockIcon = Svg("");
public static readonly string PlaneIcon = Svg("");
/// The local midnight of today, which every example lays its bars out around.
public static DateTimeOffset Today => new(DateTime.Today);
///
/// The local midnight days from today, with the offset in force on that day - which a
/// daylight saving change in between makes differ from the one carries.
///
public static DateTimeOffset Day(int days) => new(DateTime.Today.AddDays(days));
/// Gates named Gate A1, Gate A2, ... - optionally the children of a group row.
public static List Gates(int count, string terminal = "A", string? parentId = null) =>
[.. Enumerable.Range(1, count).Select(n => new BitResourceTimelineResource { Id = $"{terminal}{n}", Name = $"Gate {terminal}{n}", ParentId = parentId })];
/// Terminal A, B, ... as group rows, each holding gates.
public static List Terminals(int terminals, int gatesPerTerminal)
{
var resources = new List();
for (var t = 0; t < terminals; t++)
{
var terminal = ((char)('A' + t)).ToString();
resources.Add(new() { Id = $"T{terminal}", Name = $"Terminal {terminal}" });
resources.AddRange(Gates(gatesPerTerminal, terminal, $"T{terminal}"));
}
return resources;
}
///
/// A whole board: the resources, the days from to around today,
/// and the flights of every gate in between.
///
public static BitResourceTimelineConfig Airport(List resources, int fromDay = -7, int toDay = 7, bool labeled = false)
{
// Today is read once, so a call running across midnight gives the range and the flights the same days.
var today = DateTime.Today;
var start = new DateTimeOffset(today.AddDays(fromDay));
var end = new DateTimeOffset(today.AddDays(toDay));
return new()
{
Resources = resources,
StartDate = start,
EndDate = end,
Allocations = Flights(resources, start, end, labeled),
};
}
///
/// The turnarounds overlapping .. at every resource that is not a group
/// row: 45 minutes to 3 hours each, 30 minutes to 2 hours apart. With each carries its
/// flight number above it.
///
public static List Flights(IReadOnlyList resources, DateTimeOffset from, DateTimeOffset to, bool labeled = false)
{
var groups = resources.Where(r => r.ParentId is not null).Select(r => r.ParentId).ToHashSet();
// Counted in local calendar days rather than elapsed 24 hours, which a daylight saving change in between would
// throw off by one: the day the window starts on, through the day it ends on unless it ends at its midnight.
// Today is read once, so a call running across midnight counts the days and lays them out from the same one.
var today = DateTime.Today;
var localTo = to.LocalDateTime;
var firstDay = (int)(from.LocalDateTime.Date - today).TotalDays;
var lastDay = (int)(localTo.Date - today).TotalDays + (localTo.TimeOfDay > TimeSpan.Zero ? 1 : 0);
var flights = new List();
for (var r = 0; r < resources.Count; r++)
{
var gate = resources[r];
if (groups.Contains(gate.Id)) continue;
for (var day = firstDay; day < lastDay; day++)
{
var dayStart = new DateTimeOffset(today.AddDays(day));
var dayEnd = new DateTimeOffset(today.AddDays(day + 1));
// Seeded by the gate and the calendar date, so a day reads the same whichever window asks for it, and
// whenever it is asked for (a fetch after midnight still finds the flights the earlier ones loaded).
var random = new Random(unchecked(r * 100_003 + dayStart.Year * 10_000 + dayStart.Month * 100 + dayStart.Day));
var start = dayStart.AddMinutes(random.Next(0, 12) * 15);
for (var n = 0; ; n++)
{
var end = start.AddMinutes(random.Next(3, 13) * 15);
var flightNumber = $"{Airlines[random.Next(Airlines.Length)]}{random.Next(100, 1000)}";
if (end > dayEnd) break;
if (end > from && start < to)
{
flights.Add(new()
{
Id = $"{gate.Id}-{dayStart.ToString("yyyyMMdd", CultureInfo.InvariantCulture)}-{n}",
ResourceId = gate.Id,
StartTime = start,
EndTime = end,
TextAbove = labeled ? flightNumber : null,
});
}
start = end.AddMinutes(random.Next(2, 9) * 15);
}
}
}
return flights;
}
// An inline SVG as a data URI, so the icons need no image files.
private static string Svg(string svg) => "data:image/svg+xml," + Uri.EscapeDataString(svg);
}
```
\n**Navigation & zoom**:
```razor
/// The sample data of the BitResourceTimeline demo: the gates of an airport and the turnarounds of the flights parked
/// at them, laid out around today so the current time is always on the timeline. Every day of every gate is generated
/// from a seed of its own, so the same arguments always return the same flights - and a window asked for on demand
/// returns exactly the flights a full load would have had there.
///
public static class BitResourceTimelineDemoData
{
private static readonly string[] Airlines = ["LH", "BA", "AF", "KL", "EK", "QR", "TK", "IB"];
public static readonly string WarningIcon = Svg("");
public static readonly string CheckIcon = Svg("");
public static readonly string StarIcon = Svg("");
public static readonly string LockIcon = Svg("");
public static readonly string PlaneIcon = Svg("");
/// The local midnight of today, which every example lays its bars out around.
public static DateTimeOffset Today => new(DateTime.Today);
///
/// The local midnight days from today, with the offset in force on that day - which a
/// daylight saving change in between makes differ from the one carries.
///
public static DateTimeOffset Day(int days) => new(DateTime.Today.AddDays(days));
/// Gates named Gate A1, Gate A2, ... - optionally the children of a group row.
public static List Gates(int count, string terminal = "A", string? parentId = null) =>
[.. Enumerable.Range(1, count).Select(n => new BitResourceTimelineResource { Id = $"{terminal}{n}", Name = $"Gate {terminal}{n}", ParentId = parentId })];
/// Terminal A, B, ... as group rows, each holding gates.
public static List Terminals(int terminals, int gatesPerTerminal)
{
var resources = new List();
for (var t = 0; t < terminals; t++)
{
var terminal = ((char)('A' + t)).ToString();
resources.Add(new() { Id = $"T{terminal}", Name = $"Terminal {terminal}" });
resources.AddRange(Gates(gatesPerTerminal, terminal, $"T{terminal}"));
}
return resources;
}
///
/// A whole board: the resources, the days from to around today,
/// and the flights of every gate in between.
///
public static BitResourceTimelineConfig Airport(List resources, int fromDay = -7, int toDay = 7, bool labeled = false)
{
// Today is read once, so a call running across midnight gives the range and the flights the same days.
var today = DateTime.Today;
var start = new DateTimeOffset(today.AddDays(fromDay));
var end = new DateTimeOffset(today.AddDays(toDay));
return new()
{
Resources = resources,
StartDate = start,
EndDate = end,
Allocations = Flights(resources, start, end, labeled),
};
}
///
/// The turnarounds overlapping .. at every resource that is not a group
/// row: 45 minutes to 3 hours each, 30 minutes to 2 hours apart. With each carries its
/// flight number above it.
///
public static List Flights(IReadOnlyList resources, DateTimeOffset from, DateTimeOffset to, bool labeled = false)
{
var groups = resources.Where(r => r.ParentId is not null).Select(r => r.ParentId).ToHashSet();
// Counted in local calendar days rather than elapsed 24 hours, which a daylight saving change in between would
// throw off by one: the day the window starts on, through the day it ends on unless it ends at its midnight.
// Today is read once, so a call running across midnight counts the days and lays them out from the same one.
var today = DateTime.Today;
var localTo = to.LocalDateTime;
var firstDay = (int)(from.LocalDateTime.Date - today).TotalDays;
var lastDay = (int)(localTo.Date - today).TotalDays + (localTo.TimeOfDay > TimeSpan.Zero ? 1 : 0);
var flights = new List();
for (var r = 0; r < resources.Count; r++)
{
var gate = resources[r];
if (groups.Contains(gate.Id)) continue;
for (var day = firstDay; day < lastDay; day++)
{
var dayStart = new DateTimeOffset(today.AddDays(day));
var dayEnd = new DateTimeOffset(today.AddDays(day + 1));
// Seeded by the gate and the calendar date, so a day reads the same whichever window asks for it, and
// whenever it is asked for (a fetch after midnight still finds the flights the earlier ones loaded).
var random = new Random(unchecked(r * 100_003 + dayStart.Year * 10_000 + dayStart.Month * 100 + dayStart.Day));
var start = dayStart.AddMinutes(random.Next(0, 12) * 15);
for (var n = 0; ; n++)
{
var end = start.AddMinutes(random.Next(3, 13) * 15);
var flightNumber = $"{Airlines[random.Next(Airlines.Length)]}{random.Next(100, 1000)}";
if (end > dayEnd) break;
if (end > from && start < to)
{
flights.Add(new()
{
Id = $"{gate.Id}-{dayStart.ToString("yyyyMMdd", CultureInfo.InvariantCulture)}-{n}",
ResourceId = gate.Id,
StartTime = start,
EndTime = end,
TextAbove = labeled ? flightNumber : null,
});
}
start = end.AddMinutes(random.Next(2, 9) * 15);
}
}
}
return flights;
}
// An inline SVG as a data URI, so the icons need no image files.
private static string Svg(string svg) => "data:image/svg+xml," + Uri.EscapeDataString(svg);
}
```
\n**Selection & pointer events**:
```razor
parts = [args.Area.ToString()];
if (args.Resource is not null) parts.Add(args.Resource.Name);
if (args.Time is { } time) parts.Add(time.ToLocalTime().ToString("ddd HH:mm"));
if (args.Allocation is not null) parts.Add($"flight {args.Allocation.TextAbove}");
return string.Join(" · ", parts);
}
```
`BitResourceTimelineDemoData.cs`:
```csharp
using System.Globalization;
namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Extras.ResourceTimeline;
///
/// The sample data of the BitResourceTimeline demo: the gates of an airport and the turnarounds of the flights parked
/// at them, laid out around today so the current time is always on the timeline. Every day of every gate is generated
/// from a seed of its own, so the same arguments always return the same flights - and a window asked for on demand
/// returns exactly the flights a full load would have had there.
///
public static class BitResourceTimelineDemoData
{
private static readonly string[] Airlines = ["LH", "BA", "AF", "KL", "EK", "QR", "TK", "IB"];
public static readonly string WarningIcon = Svg("");
public static readonly string CheckIcon = Svg("");
public static readonly string StarIcon = Svg("");
public static readonly string LockIcon = Svg("");
public static readonly string PlaneIcon = Svg("");
/// The local midnight of today, which every example lays its bars out around.
public static DateTimeOffset Today => new(DateTime.Today);
///
/// The local midnight days from today, with the offset in force on that day - which a
/// daylight saving change in between makes differ from the one carries.
///
public static DateTimeOffset Day(int days) => new(DateTime.Today.AddDays(days));
/// Gates named Gate A1, Gate A2, ... - optionally the children of a group row.
public static List Gates(int count, string terminal = "A", string? parentId = null) =>
[.. Enumerable.Range(1, count).Select(n => new BitResourceTimelineResource { Id = $"{terminal}{n}", Name = $"Gate {terminal}{n}", ParentId = parentId })];
/// Terminal A, B, ... as group rows, each holding gates.
public static List Terminals(int terminals, int gatesPerTerminal)
{
var resources = new List();
for (var t = 0; t < terminals; t++)
{
var terminal = ((char)('A' + t)).ToString();
resources.Add(new() { Id = $"T{terminal}", Name = $"Terminal {terminal}" });
resources.AddRange(Gates(gatesPerTerminal, terminal, $"T{terminal}"));
}
return resources;
}
///
/// A whole board: the resources, the days from to around today,
/// and the flights of every gate in between.
///
public static BitResourceTimelineConfig Airport(List resources, int fromDay = -7, int toDay = 7, bool labeled = false)
{
// Today is read once, so a call running across midnight gives the range and the flights the same days.
var today = DateTime.Today;
var start = new DateTimeOffset(today.AddDays(fromDay));
var end = new DateTimeOffset(today.AddDays(toDay));
return new()
{
Resources = resources,
StartDate = start,
EndDate = end,
Allocations = Flights(resources, start, end, labeled),
};
}
///
/// The turnarounds overlapping .. at every resource that is not a group
/// row: 45 minutes to 3 hours each, 30 minutes to 2 hours apart. With each carries its
/// flight number above it.
///
public static List Flights(IReadOnlyList resources, DateTimeOffset from, DateTimeOffset to, bool labeled = false)
{
var groups = resources.Where(r => r.ParentId is not null).Select(r => r.ParentId).ToHashSet();
// Counted in local calendar days rather than elapsed 24 hours, which a daylight saving change in between would
// throw off by one: the day the window starts on, through the day it ends on unless it ends at its midnight.
// Today is read once, so a call running across midnight counts the days and lays them out from the same one.
var today = DateTime.Today;
var localTo = to.LocalDateTime;
var firstDay = (int)(from.LocalDateTime.Date - today).TotalDays;
var lastDay = (int)(localTo.Date - today).TotalDays + (localTo.TimeOfDay > TimeSpan.Zero ? 1 : 0);
var flights = new List();
for (var r = 0; r < resources.Count; r++)
{
var gate = resources[r];
if (groups.Contains(gate.Id)) continue;
for (var day = firstDay; day < lastDay; day++)
{
var dayStart = new DateTimeOffset(today.AddDays(day));
var dayEnd = new DateTimeOffset(today.AddDays(day + 1));
// Seeded by the gate and the calendar date, so a day reads the same whichever window asks for it, and
// whenever it is asked for (a fetch after midnight still finds the flights the earlier ones loaded).
var random = new Random(unchecked(r * 100_003 + dayStart.Year * 10_000 + dayStart.Month * 100 + dayStart.Day));
var start = dayStart.AddMinutes(random.Next(0, 12) * 15);
for (var n = 0; ; n++)
{
var end = start.AddMinutes(random.Next(3, 13) * 15);
var flightNumber = $"{Airlines[random.Next(Airlines.Length)]}{random.Next(100, 1000)}";
if (end > dayEnd) break;
if (end > from && start < to)
{
flights.Add(new()
{
Id = $"{gate.Id}-{dayStart.ToString("yyyyMMdd", CultureInfo.InvariantCulture)}-{n}",
ResourceId = gate.Id,
StartTime = start,
EndTime = end,
TextAbove = labeled ? flightNumber : null,
});
}
start = end.AddMinutes(random.Next(2, 9) * 15);
}
}
}
return flights;
}
// An inline SVG as a data URI, so the icons need no image files.
private static string Svg(string svg) => "data:image/svg+xml," + Uri.EscapeDataString(svg);
}
```
\n**Editing**:
```razor
editLog = [];
private int createdCount;
private static BitResourceTimelineConfig CreateEditingConfig()
{
var config = BitResourceTimelineDemoData.Airport(BitResourceTimelineDemoData.Gates(6), labeled: true);
var now = DateTimeOffset.Now;
// A flight at its gate right now cannot be moved: Locked keeps it selectable but not editable.
foreach (var flight in config.Allocations.Where(f => f.StartTime <= now && f.EndTime > now))
{
flight.Locked = true;
flight.Color = "#868e96";
flight.TextBelow = "at the gate";
}
return config;
}
private static BitResourceTimelineOptions CreateEditingOptions(bool allowResourceChange) => new()
{
AutoScrollToNow = true,
Editable = true,
EditSnapMinutes = 15,
AllowResourceChange = allowResourceChange,
EmptyDragAction = BitResourceTimelineEmptyDragAction.Create,
AllowDelete = true,
};
private Task HandleAllocationChanging(BitResourceTimelineAllocationChange change) => Task.FromResult(AcceptEdit([change]));
private Task HandleAllocationsChanging(IReadOnlyList changes) => Task.FromResult(AcceptEdit(changes));
// The past is history: an edit that would start a flight before now is refused, and the bars snap back.
private bool AcceptEdit(IReadOnlyList changes)
{
var refused = changes.FirstOrDefault(c => c.Allocation.StartTime < DateTimeOffset.Now);
if (refused is null) return true;
Log($"Refused: {refused.Allocation.TextAbove} would start in the past");
return false;
}
private void HandleAllocationChanged(BitResourceTimelineAllocation flight)
{
Log($"{flight.TextAbove}: Gate {flight.ResourceId}, {flight.StartTime.ToLocalTime():ddd HH:mm} - {flight.EndTime.ToLocalTime():HH:mm}");
}
private Task HandleAllocationCreating(BitResourceTimelineCreateRequest request)
{
// The timeline never invents ids: the app builds the allocation (or returns null to cancel).
var flight = new BitResourceTimelineAllocation
{
Id = $"new-{++createdCount}",
ResourceId = request.ResourceId,
StartTime = request.StartTime,
EndTime = request.EndTime,
Color = "#2f9e44",
TextAbove = $"NEW{createdCount}",
};
editingConfig.Allocations.Add(flight);
Log($"Created {flight.TextAbove} at Gate {flight.ResourceId}");
return Task.FromResult(flight);
}
private Task?> HandleAllocationsCopying(BitResourceTimelineCopyRequest request)
{
List copies =
[
.. request.Allocations.Select(source => new BitResourceTimelineAllocation
{
Id = $"copy-{++createdCount}",
ResourceId = source.ResourceId,
StartTime = source.StartTime + request.Offset,
EndTime = source.EndTime + request.Offset,
Color = "#2f9e44",
TextAbove = $"{source.TextAbove} copy",
})
];
editingConfig.Allocations.AddRange(copies);
Log($"Pasted {copies.Count}");
return Task.FromResult?>(copies);
}
private Task HandleAllocationsDeleting(IReadOnlyList ids)
{
if (editingConfig.Allocations.Any(f => f.Locked && ids.Contains(f.Id)))
{
Log("Refused: a flight at its gate cannot be deleted");
return Task.FromResult(false);
}
editingConfig.Allocations.RemoveAll(f => ids.Contains(f.Id));
Log($"Deleted {ids.Count}");
return Task.FromResult(true);
}
private void Log(string line)
{
editLog.Insert(0, line);
if (editLog.Count > 5) editLog.RemoveAt(5);
StateHasChanged();
}
```
`BitResourceTimelineDemoData.cs`:
```csharp
using System.Globalization;
namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Extras.ResourceTimeline;
///
/// The sample data of the BitResourceTimeline demo: the gates of an airport and the turnarounds of the flights parked
/// at them, laid out around today so the current time is always on the timeline. Every day of every gate is generated
/// from a seed of its own, so the same arguments always return the same flights - and a window asked for on demand
/// returns exactly the flights a full load would have had there.
///
public static class BitResourceTimelineDemoData
{
private static readonly string[] Airlines = ["LH", "BA", "AF", "KL", "EK", "QR", "TK", "IB"];
public static readonly string WarningIcon = Svg("");
public static readonly string CheckIcon = Svg("");
public static readonly string StarIcon = Svg("");
public static readonly string LockIcon = Svg("");
public static readonly string PlaneIcon = Svg("");
/// The local midnight of today, which every example lays its bars out around.
public static DateTimeOffset Today => new(DateTime.Today);
///
/// The local midnight days from today, with the offset in force on that day - which a
/// daylight saving change in between makes differ from the one carries.
///
public static DateTimeOffset Day(int days) => new(DateTime.Today.AddDays(days));
/// Gates named Gate A1, Gate A2, ... - optionally the children of a group row.
public static List Gates(int count, string terminal = "A", string? parentId = null) =>
[.. Enumerable.Range(1, count).Select(n => new BitResourceTimelineResource { Id = $"{terminal}{n}", Name = $"Gate {terminal}{n}", ParentId = parentId })];
/// Terminal A, B, ... as group rows, each holding gates.
public static List Terminals(int terminals, int gatesPerTerminal)
{
var resources = new List();
for (var t = 0; t < terminals; t++)
{
var terminal = ((char)('A' + t)).ToString();
resources.Add(new() { Id = $"T{terminal}", Name = $"Terminal {terminal}" });
resources.AddRange(Gates(gatesPerTerminal, terminal, $"T{terminal}"));
}
return resources;
}
///
/// A whole board: the resources, the days from to around today,
/// and the flights of every gate in between.
///
public static BitResourceTimelineConfig Airport(List resources, int fromDay = -7, int toDay = 7, bool labeled = false)
{
// Today is read once, so a call running across midnight gives the range and the flights the same days.
var today = DateTime.Today;
var start = new DateTimeOffset(today.AddDays(fromDay));
var end = new DateTimeOffset(today.AddDays(toDay));
return new()
{
Resources = resources,
StartDate = start,
EndDate = end,
Allocations = Flights(resources, start, end, labeled),
};
}
///
/// The turnarounds overlapping .. at every resource that is not a group
/// row: 45 minutes to 3 hours each, 30 minutes to 2 hours apart. With each carries its
/// flight number above it.
///
public static List Flights(IReadOnlyList resources, DateTimeOffset from, DateTimeOffset to, bool labeled = false)
{
var groups = resources.Where(r => r.ParentId is not null).Select(r => r.ParentId).ToHashSet();
// Counted in local calendar days rather than elapsed 24 hours, which a daylight saving change in between would
// throw off by one: the day the window starts on, through the day it ends on unless it ends at its midnight.
// Today is read once, so a call running across midnight counts the days and lays them out from the same one.
var today = DateTime.Today;
var localTo = to.LocalDateTime;
var firstDay = (int)(from.LocalDateTime.Date - today).TotalDays;
var lastDay = (int)(localTo.Date - today).TotalDays + (localTo.TimeOfDay > TimeSpan.Zero ? 1 : 0);
var flights = new List();
for (var r = 0; r < resources.Count; r++)
{
var gate = resources[r];
if (groups.Contains(gate.Id)) continue;
for (var day = firstDay; day < lastDay; day++)
{
var dayStart = new DateTimeOffset(today.AddDays(day));
var dayEnd = new DateTimeOffset(today.AddDays(day + 1));
// Seeded by the gate and the calendar date, so a day reads the same whichever window asks for it, and
// whenever it is asked for (a fetch after midnight still finds the flights the earlier ones loaded).
var random = new Random(unchecked(r * 100_003 + dayStart.Year * 10_000 + dayStart.Month * 100 + dayStart.Day));
var start = dayStart.AddMinutes(random.Next(0, 12) * 15);
for (var n = 0; ; n++)
{
var end = start.AddMinutes(random.Next(3, 13) * 15);
var flightNumber = $"{Airlines[random.Next(Airlines.Length)]}{random.Next(100, 1000)}";
if (end > dayEnd) break;
if (end > from && start < to)
{
flights.Add(new()
{
Id = $"{gate.Id}-{dayStart.ToString("yyyyMMdd", CultureInfo.InvariantCulture)}-{n}",
ResourceId = gate.Id,
StartTime = start,
EndTime = end,
TextAbove = labeled ? flightNumber : null,
});
}
start = end.AddMinutes(random.Next(2, 9) * 15);
}
}
}
return flights;
}
// An inline SVG as a data URI, so the icons need no image files.
private static string Svg(string svg) => "data:image/svg+xml," + Uri.EscapeDataString(svg);
}
```
\n**Overlap & stacking**:
```razor
```
```csharp
private readonly BitResourceTimelineConfig overlapConfig = CreateOverlapConfig();
private int maxStackLanes;
private bool stackLabelClearance = true;
private bool stackOnLabelCollision = true;
private bool allowOverlap = true;
private BitResourceTimelineOptions overlapOptions = CreateOverlapOptions(0, true, true, true);
private void UpdateOverlapOptions() => overlapOptions = CreateOverlapOptions(maxStackLanes, stackLabelClearance, stackOnLabelCollision, allowOverlap);
private static BitResourceTimelineOptions CreateOverlapOptions(int maxStackLanes, bool stackLabelClearance, bool stackOnLabelCollision, bool allowOverlap) => new()
{
Editable = true,
BarHeight = 8,
BarMargin = 4,
MaxStackLanes = maxStackLanes,
StackLabelClearance = stackLabelClearance,
StackOnLabelCollision = stackOnLabelCollision,
AllowOverlap = allowOverlap,
};
private static BitResourceTimelineConfig CreateOverlapConfig()
{
var today = BitResourceTimelineDemoData.Today;
BitResourceTimelineAllocation Flight(string gate, string id, double from, double to, string? below = null, bool times = false) => new()
{
Id = id,
ResourceId = gate,
StartTime = today.AddHours(from),
EndTime = today.AddHours(to),
TextAbove = id,
TextBelow = below,
TextStart = times ? today.AddHours(from).ToString("HH:mm", CultureInfo.InvariantCulture) : null,
TextEnd = times ? today.AddHours(to).ToString("HH:mm", CultureInfo.InvariantCulture) : null,
};
return new()
{
StartDate = today,
EndDate = today.AddDays(1),
Resources = BitResourceTimelineDemoData.Gates(4),
Allocations =
[
// Five at once: five lanes, or MaxStackLanes and a +N marker.
Flight("A1", "LH400", 2, 7),
Flight("A1", "LH402", 3, 8),
Flight("A1", "LH404", 3.5, 6),
Flight("A1", "LH406", 4, 9),
Flight("A1", "LH408", 5, 7.5),
// A quarter of an hour apart: their times collide, though they do not overlap.
Flight("A2", "BA200", 3, 6, times: true),
Flight("A2", "BA202", 6.25, 9, times: true),
// Overlapping, with labels above and below.
Flight("A3", "AF300", 2, 7, below: "Paris"),
Flight("A3", "AF302", 4, 9, below: "Nice"),
// Drop a bar on this one to try AllowOverlap.
Flight("A4", "KL500", 3, 6),
],
};
}
```
`BitResourceTimelineDemoData.cs`:
```csharp
using System.Globalization;
namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Extras.ResourceTimeline;
///
/// The sample data of the BitResourceTimeline demo: the gates of an airport and the turnarounds of the flights parked
/// at them, laid out around today so the current time is always on the timeline. Every day of every gate is generated
/// from a seed of its own, so the same arguments always return the same flights - and a window asked for on demand
/// returns exactly the flights a full load would have had there.
///
public static class BitResourceTimelineDemoData
{
private static readonly string[] Airlines = ["LH", "BA", "AF", "KL", "EK", "QR", "TK", "IB"];
public static readonly string WarningIcon = Svg("");
public static readonly string CheckIcon = Svg("");
public static readonly string StarIcon = Svg("");
public static readonly string LockIcon = Svg("");
public static readonly string PlaneIcon = Svg("");
/// The local midnight of today, which every example lays its bars out around.
public static DateTimeOffset Today => new(DateTime.Today);
///
/// The local midnight days from today, with the offset in force on that day - which a
/// daylight saving change in between makes differ from the one carries.
///
public static DateTimeOffset Day(int days) => new(DateTime.Today.AddDays(days));
/// Gates named Gate A1, Gate A2, ... - optionally the children of a group row.
public static List Gates(int count, string terminal = "A", string? parentId = null) =>
[.. Enumerable.Range(1, count).Select(n => new BitResourceTimelineResource { Id = $"{terminal}{n}", Name = $"Gate {terminal}{n}", ParentId = parentId })];
/// Terminal A, B, ... as group rows, each holding gates.
public static List Terminals(int terminals, int gatesPerTerminal)
{
var resources = new List();
for (var t = 0; t < terminals; t++)
{
var terminal = ((char)('A' + t)).ToString();
resources.Add(new() { Id = $"T{terminal}", Name = $"Terminal {terminal}" });
resources.AddRange(Gates(gatesPerTerminal, terminal, $"T{terminal}"));
}
return resources;
}
///
/// A whole board: the resources, the days from to around today,
/// and the flights of every gate in between.
///
public static BitResourceTimelineConfig Airport(List resources, int fromDay = -7, int toDay = 7, bool labeled = false)
{
// Today is read once, so a call running across midnight gives the range and the flights the same days.
var today = DateTime.Today;
var start = new DateTimeOffset(today.AddDays(fromDay));
var end = new DateTimeOffset(today.AddDays(toDay));
return new()
{
Resources = resources,
StartDate = start,
EndDate = end,
Allocations = Flights(resources, start, end, labeled),
};
}
///
/// The turnarounds overlapping .. at every resource that is not a group
/// row: 45 minutes to 3 hours each, 30 minutes to 2 hours apart. With each carries its
/// flight number above it.
///
public static List Flights(IReadOnlyList resources, DateTimeOffset from, DateTimeOffset to, bool labeled = false)
{
var groups = resources.Where(r => r.ParentId is not null).Select(r => r.ParentId).ToHashSet();
// Counted in local calendar days rather than elapsed 24 hours, which a daylight saving change in between would
// throw off by one: the day the window starts on, through the day it ends on unless it ends at its midnight.
// Today is read once, so a call running across midnight counts the days and lays them out from the same one.
var today = DateTime.Today;
var localTo = to.LocalDateTime;
var firstDay = (int)(from.LocalDateTime.Date - today).TotalDays;
var lastDay = (int)(localTo.Date - today).TotalDays + (localTo.TimeOfDay > TimeSpan.Zero ? 1 : 0);
var flights = new List();
for (var r = 0; r < resources.Count; r++)
{
var gate = resources[r];
if (groups.Contains(gate.Id)) continue;
for (var day = firstDay; day < lastDay; day++)
{
var dayStart = new DateTimeOffset(today.AddDays(day));
var dayEnd = new DateTimeOffset(today.AddDays(day + 1));
// Seeded by the gate and the calendar date, so a day reads the same whichever window asks for it, and
// whenever it is asked for (a fetch after midnight still finds the flights the earlier ones loaded).
var random = new Random(unchecked(r * 100_003 + dayStart.Year * 10_000 + dayStart.Month * 100 + dayStart.Day));
var start = dayStart.AddMinutes(random.Next(0, 12) * 15);
for (var n = 0; ; n++)
{
var end = start.AddMinutes(random.Next(3, 13) * 15);
var flightNumber = $"{Airlines[random.Next(Airlines.Length)]}{random.Next(100, 1000)}";
if (end > dayEnd) break;
if (end > from && start < to)
{
flights.Add(new()
{
Id = $"{gate.Id}-{dayStart.ToString("yyyyMMdd", CultureInfo.InvariantCulture)}-{n}",
ResourceId = gate.Id,
StartTime = start,
EndTime = end,
TextAbove = labeled ? flightNumber : null,
});
}
start = end.AddMinutes(random.Next(2, 9) * 15);
}
}
}
return flights;
}
// An inline SVG as a data URI, so the icons need no image files.
private static string Svg(string svg) => "data:image/svg+xml," + Uri.EscapeDataString(svg);
}
```
\n**Working time & surface**:
```razor
```
```csharp
private readonly BitResourceTimelineConfig surfaceConfig = CreateSurfaceConfig();
private string? surfacePreset = "days";
private bool axisLines;
private BitResourceTimelineOptions surfaceOptions = CreateSurfaceOptions("days", false);
private void UpdateSurfaceOptions() => surfaceOptions = CreateSurfaceOptions(surfacePreset, axisLines);
private static BitResourceTimelineConfig CreateSurfaceConfig()
{
var config = BitResourceTimelineDemoData.Airport(BitResourceTimelineDemoData.Gates(6), labeled: true);
// A resource paints its own row and line, over any pattern of the surface.
config.Resources[2].Name = "Gate A3 (closed)";
config.Resources[2].Background = "rgba(250, 82, 82, 0.15)";
config.Allocations.RemoveAll(f => f.ResourceId == "A3");
config.Resources[4].LineColor = "#e8590c";
return config;
}
private static BitResourceTimelineOptions CreateSurfaceOptions(string? preset, bool axisLines)
{
var today = BitResourceTimelineDemoData.Today;
return new()
{
AutoScrollToNow = true,
// The weekends, and the hours outside 09:00 - 17:00, are washed over whatever the surface paints.
NonWorkingDays = [0, 6],
WorkingHoursStart = 9 * 60,
WorkingHoursEnd = 17 * 60,
// A new surface replaces the previous one as a whole, and an empty one clears it.
Surface = preset switch
{
"days" => new()
{
Columns = new() { Colors = ["rgba(25, 113, 194, 0.08)", "transparent"] },
ShadeTimeAxis = true,
},
"shifts" => new()
{
Columns = new()
{
Unit = BitResourceTimelineSurfaceUnit.Hour,
Span = 8,
Align = BitResourceTimelineStripeAlign.Repeat,
Colors = ["rgba(250, 176, 5, 0.12)", "rgba(47, 158, 68, 0.1)", "rgba(25, 113, 194, 0.1)"],
},
ShadeTimeAxis = true,
},
"rows" => new()
{
Rows = new() { Colors = ["rgba(134, 142, 150, 0.12)", "transparent"] },
ResourceColors = new() { ["A6"] = "rgba(156, 54, 181, 0.15)" },
ShadeResourceAxis = true,
},
"checker" => new()
{
Columns = new() { Colors = ["rgba(25, 113, 194, 0.1)", "transparent"] },
Combine = BitResourceTimelineSurfaceCombine.Checker,
},
"bands" => new()
{
Bands =
[
new() { Start = today.AddHours(12), End = today.AddHours(14), Color = "rgba(250, 82, 82, 0.15)" },
new() { Start = today.AddHours(6), End = today.AddHours(10), Color = "rgba(250, 176, 5, 0.3)", ResourceId = "A2" },
],
},
_ => new(),
},
AxisLines = axisLines
? new()
{
BoundariesOnly = true,
Vertical = new() { Colors = ["#1971c2"] },
Horizontal = new() { Colors = ["rgba(134, 142, 150, 0.6)"] },
}
: new(),
};
}
```
`BitResourceTimelineDemoData.cs`:
```csharp
using System.Globalization;
namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Extras.ResourceTimeline;
///
/// The sample data of the BitResourceTimeline demo: the gates of an airport and the turnarounds of the flights parked
/// at them, laid out around today so the current time is always on the timeline. Every day of every gate is generated
/// from a seed of its own, so the same arguments always return the same flights - and a window asked for on demand
/// returns exactly the flights a full load would have had there.
///
public static class BitResourceTimelineDemoData
{
private static readonly string[] Airlines = ["LH", "BA", "AF", "KL", "EK", "QR", "TK", "IB"];
public static readonly string WarningIcon = Svg("");
public static readonly string CheckIcon = Svg("");
public static readonly string StarIcon = Svg("");
public static readonly string LockIcon = Svg("");
public static readonly string PlaneIcon = Svg("");
/// The local midnight of today, which every example lays its bars out around.
public static DateTimeOffset Today => new(DateTime.Today);
///
/// The local midnight days from today, with the offset in force on that day - which a
/// daylight saving change in between makes differ from the one carries.
///
public static DateTimeOffset Day(int days) => new(DateTime.Today.AddDays(days));
/// Gates named Gate A1, Gate A2, ... - optionally the children of a group row.
public static List Gates(int count, string terminal = "A", string? parentId = null) =>
[.. Enumerable.Range(1, count).Select(n => new BitResourceTimelineResource { Id = $"{terminal}{n}", Name = $"Gate {terminal}{n}", ParentId = parentId })];
/// Terminal A, B, ... as group rows, each holding gates.
public static List Terminals(int terminals, int gatesPerTerminal)
{
var resources = new List();
for (var t = 0; t < terminals; t++)
{
var terminal = ((char)('A' + t)).ToString();
resources.Add(new() { Id = $"T{terminal}", Name = $"Terminal {terminal}" });
resources.AddRange(Gates(gatesPerTerminal, terminal, $"T{terminal}"));
}
return resources;
}
///
/// A whole board: the resources, the days from to around today,
/// and the flights of every gate in between.
///
public static BitResourceTimelineConfig Airport(List resources, int fromDay = -7, int toDay = 7, bool labeled = false)
{
// Today is read once, so a call running across midnight gives the range and the flights the same days.
var today = DateTime.Today;
var start = new DateTimeOffset(today.AddDays(fromDay));
var end = new DateTimeOffset(today.AddDays(toDay));
return new()
{
Resources = resources,
StartDate = start,
EndDate = end,
Allocations = Flights(resources, start, end, labeled),
};
}
///
/// The turnarounds overlapping .. at every resource that is not a group
/// row: 45 minutes to 3 hours each, 30 minutes to 2 hours apart. With each carries its
/// flight number above it.
///
public static List Flights(IReadOnlyList resources, DateTimeOffset from, DateTimeOffset to, bool labeled = false)
{
var groups = resources.Where(r => r.ParentId is not null).Select(r => r.ParentId).ToHashSet();
// Counted in local calendar days rather than elapsed 24 hours, which a daylight saving change in between would
// throw off by one: the day the window starts on, through the day it ends on unless it ends at its midnight.
// Today is read once, so a call running across midnight counts the days and lays them out from the same one.
var today = DateTime.Today;
var localTo = to.LocalDateTime;
var firstDay = (int)(from.LocalDateTime.Date - today).TotalDays;
var lastDay = (int)(localTo.Date - today).TotalDays + (localTo.TimeOfDay > TimeSpan.Zero ? 1 : 0);
var flights = new List();
for (var r = 0; r < resources.Count; r++)
{
var gate = resources[r];
if (groups.Contains(gate.Id)) continue;
for (var day = firstDay; day < lastDay; day++)
{
var dayStart = new DateTimeOffset(today.AddDays(day));
var dayEnd = new DateTimeOffset(today.AddDays(day + 1));
// Seeded by the gate and the calendar date, so a day reads the same whichever window asks for it, and
// whenever it is asked for (a fetch after midnight still finds the flights the earlier ones loaded).
var random = new Random(unchecked(r * 100_003 + dayStart.Year * 10_000 + dayStart.Month * 100 + dayStart.Day));
var start = dayStart.AddMinutes(random.Next(0, 12) * 15);
for (var n = 0; ; n++)
{
var end = start.AddMinutes(random.Next(3, 13) * 15);
var flightNumber = $"{Airlines[random.Next(Airlines.Length)]}{random.Next(100, 1000)}";
if (end > dayEnd) break;
if (end > from && start < to)
{
flights.Add(new()
{
Id = $"{gate.Id}-{dayStart.ToString("yyyyMMdd", CultureInfo.InvariantCulture)}-{n}",
ResourceId = gate.Id,
StartTime = start,
EndTime = end,
TextAbove = labeled ? flightNumber : null,
});
}
start = end.AddMinutes(random.Next(2, 9) * 15);
}
}
}
return flights;
}
// An inline SVG as a data URI, so the icons need no image files.
private static string Svg(string svg) => "data:image/svg+xml," + Uri.EscapeDataString(svg);
}
```
\n**Time zone & locale**:
```razor
```
```csharp
private BitResourceTimeline zoneRef = default!;
private readonly BitResourceTimelineConfig zoneConfig = BitResourceTimelineDemoData.Airport(BitResourceTimelineDemoData.Gates(5), fromDay: -21, toDay: 21, labeled: true);
private string? timeZone = "";
private string? locale = "";
private DayOfWeek firstDayOfWeek = DayOfWeek.Monday;
private bool showUtcTime;
private bool hour12;
private BitResourceTimelineOptions zoneOptions = CreateZoneOptions("", "", DayOfWeek.Monday, false, false);
private void UpdateZoneOptions() => zoneOptions = CreateZoneOptions(timeZone, locale, firstDayOfWeek, showUtcTime, hour12);
private static BitResourceTimelineOptions CreateZoneOptions(string? timeZone, string? locale, DayOfWeek firstDayOfWeek, bool showUtcTime, bool hour12) => new()
{
AutoScrollToNow = true,
// An empty string is the viewer's own zone and locale: a null option would keep the one applied before.
TimeZone = timeZone,
Locale = locale,
FirstDayOfWeek = firstDayOfWeek,
ShowUtcTime = showUtcTime,
// The UTC row shares the band under the day labels, so the axis grows with it.
TimeAxisHeight = showUtcTime ? 80 : 60,
Hour12 = hour12,
NonWorkingDays = [0, 6],
Surface = new()
{
Columns = new() { Unit = BitResourceTimelineSurfaceUnit.Week, Colors = ["rgba(25, 113, 194, 0.1)", "transparent"] },
ShadeTimeAxis = true,
},
};
```
`BitResourceTimelineDemoData.cs`:
```csharp
using System.Globalization;
namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Extras.ResourceTimeline;
///
/// The sample data of the BitResourceTimeline demo: the gates of an airport and the turnarounds of the flights parked
/// at them, laid out around today so the current time is always on the timeline. Every day of every gate is generated
/// from a seed of its own, so the same arguments always return the same flights - and a window asked for on demand
/// returns exactly the flights a full load would have had there.
///
public static class BitResourceTimelineDemoData
{
private static readonly string[] Airlines = ["LH", "BA", "AF", "KL", "EK", "QR", "TK", "IB"];
public static readonly string WarningIcon = Svg("");
public static readonly string CheckIcon = Svg("");
public static readonly string StarIcon = Svg("");
public static readonly string LockIcon = Svg("");
public static readonly string PlaneIcon = Svg("");
/// The local midnight of today, which every example lays its bars out around.
public static DateTimeOffset Today => new(DateTime.Today);
///
/// The local midnight days from today, with the offset in force on that day - which a
/// daylight saving change in between makes differ from the one carries.
///
public static DateTimeOffset Day(int days) => new(DateTime.Today.AddDays(days));
/// Gates named Gate A1, Gate A2, ... - optionally the children of a group row.
public static List Gates(int count, string terminal = "A", string? parentId = null) =>
[.. Enumerable.Range(1, count).Select(n => new BitResourceTimelineResource { Id = $"{terminal}{n}", Name = $"Gate {terminal}{n}", ParentId = parentId })];
/// Terminal A, B, ... as group rows, each holding gates.
public static List Terminals(int terminals, int gatesPerTerminal)
{
var resources = new List();
for (var t = 0; t < terminals; t++)
{
var terminal = ((char)('A' + t)).ToString();
resources.Add(new() { Id = $"T{terminal}", Name = $"Terminal {terminal}" });
resources.AddRange(Gates(gatesPerTerminal, terminal, $"T{terminal}"));
}
return resources;
}
///
/// A whole board: the resources, the days from to around today,
/// and the flights of every gate in between.
///
public static BitResourceTimelineConfig Airport(List resources, int fromDay = -7, int toDay = 7, bool labeled = false)
{
// Today is read once, so a call running across midnight gives the range and the flights the same days.
var today = DateTime.Today;
var start = new DateTimeOffset(today.AddDays(fromDay));
var end = new DateTimeOffset(today.AddDays(toDay));
return new()
{
Resources = resources,
StartDate = start,
EndDate = end,
Allocations = Flights(resources, start, end, labeled),
};
}
///
/// The turnarounds overlapping .. at every resource that is not a group
/// row: 45 minutes to 3 hours each, 30 minutes to 2 hours apart. With each carries its
/// flight number above it.
///
public static List Flights(IReadOnlyList resources, DateTimeOffset from, DateTimeOffset to, bool labeled = false)
{
var groups = resources.Where(r => r.ParentId is not null).Select(r => r.ParentId).ToHashSet();
// Counted in local calendar days rather than elapsed 24 hours, which a daylight saving change in between would
// throw off by one: the day the window starts on, through the day it ends on unless it ends at its midnight.
// Today is read once, so a call running across midnight counts the days and lays them out from the same one.
var today = DateTime.Today;
var localTo = to.LocalDateTime;
var firstDay = (int)(from.LocalDateTime.Date - today).TotalDays;
var lastDay = (int)(localTo.Date - today).TotalDays + (localTo.TimeOfDay > TimeSpan.Zero ? 1 : 0);
var flights = new List();
for (var r = 0; r < resources.Count; r++)
{
var gate = resources[r];
if (groups.Contains(gate.Id)) continue;
for (var day = firstDay; day < lastDay; day++)
{
var dayStart = new DateTimeOffset(today.AddDays(day));
var dayEnd = new DateTimeOffset(today.AddDays(day + 1));
// Seeded by the gate and the calendar date, so a day reads the same whichever window asks for it, and
// whenever it is asked for (a fetch after midnight still finds the flights the earlier ones loaded).
var random = new Random(unchecked(r * 100_003 + dayStart.Year * 10_000 + dayStart.Month * 100 + dayStart.Day));
var start = dayStart.AddMinutes(random.Next(0, 12) * 15);
for (var n = 0; ; n++)
{
var end = start.AddMinutes(random.Next(3, 13) * 15);
var flightNumber = $"{Airlines[random.Next(Airlines.Length)]}{random.Next(100, 1000)}";
if (end > dayEnd) break;
if (end > from && start < to)
{
flights.Add(new()
{
Id = $"{gate.Id}-{dayStart.ToString("yyyyMMdd", CultureInfo.InvariantCulture)}-{n}",
ResourceId = gate.Id,
StartTime = start,
EndTime = end,
TextAbove = labeled ? flightNumber : null,
});
}
start = end.AddMinutes(random.Next(2, 9) * 15);
}
}
}
return flights;
}
// An inline SVG as a data URI, so the icons need no image files.
private static string Svg(string svg) => "data:image/svg+xml," + Uri.EscapeDataString(svg);
}
```
\n**Templates**:
```razor
@if (row.HasChildren)
{
@row.Resource.Name
}
else
{
```
```csharp
private BitResourceTimelineConfig templatesConfig = CreateTemplatesConfig();
// The axis in UTC, the zone the TooltipTemplate formats its times in.
private readonly BitResourceTimelineOptions templatesOptions = new() { TimeZone = "UTC", AutoScrollToNow = true, PreserveScrollOnReload = true };
private static BitResourceTimelineConfig CreateTemplatesConfig() => BitResourceTimelineDemoData.Airport(BitResourceTimelineDemoData.Terminals(2, 4), labeled: true);
private int FlightsOf(string resourceId) => templatesConfig.Allocations.Count(f => f.ResourceId == resourceId);
```
`BitResourceTimelineDemoData.cs`:
```csharp
using System.Globalization;
namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Extras.ResourceTimeline;
///
/// The sample data of the BitResourceTimeline demo: the gates of an airport and the turnarounds of the flights parked
/// at them, laid out around today so the current time is always on the timeline. Every day of every gate is generated
/// from a seed of its own, so the same arguments always return the same flights - and a window asked for on demand
/// returns exactly the flights a full load would have had there.
///
public static class BitResourceTimelineDemoData
{
private static readonly string[] Airlines = ["LH", "BA", "AF", "KL", "EK", "QR", "TK", "IB"];
public static readonly string WarningIcon = Svg("");
public static readonly string CheckIcon = Svg("");
public static readonly string StarIcon = Svg("");
public static readonly string LockIcon = Svg("");
public static readonly string PlaneIcon = Svg("");
/// The local midnight of today, which every example lays its bars out around.
public static DateTimeOffset Today => new(DateTime.Today);
///
/// The local midnight days from today, with the offset in force on that day - which a
/// daylight saving change in between makes differ from the one carries.
///
public static DateTimeOffset Day(int days) => new(DateTime.Today.AddDays(days));
/// Gates named Gate A1, Gate A2, ... - optionally the children of a group row.
public static List Gates(int count, string terminal = "A", string? parentId = null) =>
[.. Enumerable.Range(1, count).Select(n => new BitResourceTimelineResource { Id = $"{terminal}{n}", Name = $"Gate {terminal}{n}", ParentId = parentId })];
/// Terminal A, B, ... as group rows, each holding gates.
public static List Terminals(int terminals, int gatesPerTerminal)
{
var resources = new List();
for (var t = 0; t < terminals; t++)
{
var terminal = ((char)('A' + t)).ToString();
resources.Add(new() { Id = $"T{terminal}", Name = $"Terminal {terminal}" });
resources.AddRange(Gates(gatesPerTerminal, terminal, $"T{terminal}"));
}
return resources;
}
///
/// A whole board: the resources, the days from to around today,
/// and the flights of every gate in between.
///
public static BitResourceTimelineConfig Airport(List resources, int fromDay = -7, int toDay = 7, bool labeled = false)
{
// Today is read once, so a call running across midnight gives the range and the flights the same days.
var today = DateTime.Today;
var start = new DateTimeOffset(today.AddDays(fromDay));
var end = new DateTimeOffset(today.AddDays(toDay));
return new()
{
Resources = resources,
StartDate = start,
EndDate = end,
Allocations = Flights(resources, start, end, labeled),
};
}
///
/// The turnarounds overlapping .. at every resource that is not a group
/// row: 45 minutes to 3 hours each, 30 minutes to 2 hours apart. With each carries its
/// flight number above it.
///
public static List Flights(IReadOnlyList resources, DateTimeOffset from, DateTimeOffset to, bool labeled = false)
{
var groups = resources.Where(r => r.ParentId is not null).Select(r => r.ParentId).ToHashSet();
// Counted in local calendar days rather than elapsed 24 hours, which a daylight saving change in between would
// throw off by one: the day the window starts on, through the day it ends on unless it ends at its midnight.
// Today is read once, so a call running across midnight counts the days and lays them out from the same one.
var today = DateTime.Today;
var localTo = to.LocalDateTime;
var firstDay = (int)(from.LocalDateTime.Date - today).TotalDays;
var lastDay = (int)(localTo.Date - today).TotalDays + (localTo.TimeOfDay > TimeSpan.Zero ? 1 : 0);
var flights = new List();
for (var r = 0; r < resources.Count; r++)
{
var gate = resources[r];
if (groups.Contains(gate.Id)) continue;
for (var day = firstDay; day < lastDay; day++)
{
var dayStart = new DateTimeOffset(today.AddDays(day));
var dayEnd = new DateTimeOffset(today.AddDays(day + 1));
// Seeded by the gate and the calendar date, so a day reads the same whichever window asks for it, and
// whenever it is asked for (a fetch after midnight still finds the flights the earlier ones loaded).
var random = new Random(unchecked(r * 100_003 + dayStart.Year * 10_000 + dayStart.Month * 100 + dayStart.Day));
var start = dayStart.AddMinutes(random.Next(0, 12) * 15);
for (var n = 0; ; n++)
{
var end = start.AddMinutes(random.Next(3, 13) * 15);
var flightNumber = $"{Airlines[random.Next(Airlines.Length)]}{random.Next(100, 1000)}";
if (end > dayEnd) break;
if (end > from && start < to)
{
flights.Add(new()
{
Id = $"{gate.Id}-{dayStart.ToString("yyyyMMdd", CultureInfo.InvariantCulture)}-{n}",
ResourceId = gate.Id,
StartTime = start,
EndTime = end,
TextAbove = labeled ? flightNumber : null,
});
}
start = end.AddMinutes(random.Next(2, 9) * 15);
}
}
}
return flights;
}
// An inline SVG as a data URI, so the icons need no image files.
private static string Svg(string svg) => "data:image/svg+xml," + Uri.EscapeDataString(svg);
}
```
\n**Resource column**:
```razor
/// The sample data of the BitResourceTimeline demo: the gates of an airport and the turnarounds of the flights parked
/// at them, laid out around today so the current time is always on the timeline. Every day of every gate is generated
/// from a seed of its own, so the same arguments always return the same flights - and a window asked for on demand
/// returns exactly the flights a full load would have had there.
///
public static class BitResourceTimelineDemoData
{
private static readonly string[] Airlines = ["LH", "BA", "AF", "KL", "EK", "QR", "TK", "IB"];
public static readonly string WarningIcon = Svg("");
public static readonly string CheckIcon = Svg("");
public static readonly string StarIcon = Svg("");
public static readonly string LockIcon = Svg("");
public static readonly string PlaneIcon = Svg("");
/// The local midnight of today, which every example lays its bars out around.
public static DateTimeOffset Today => new(DateTime.Today);
///
/// The local midnight days from today, with the offset in force on that day - which a
/// daylight saving change in between makes differ from the one carries.
///
public static DateTimeOffset Day(int days) => new(DateTime.Today.AddDays(days));
/// Gates named Gate A1, Gate A2, ... - optionally the children of a group row.
public static List Gates(int count, string terminal = "A", string? parentId = null) =>
[.. Enumerable.Range(1, count).Select(n => new BitResourceTimelineResource { Id = $"{terminal}{n}", Name = $"Gate {terminal}{n}", ParentId = parentId })];
/// Terminal A, B, ... as group rows, each holding gates.
public static List Terminals(int terminals, int gatesPerTerminal)
{
var resources = new List();
for (var t = 0; t < terminals; t++)
{
var terminal = ((char)('A' + t)).ToString();
resources.Add(new() { Id = $"T{terminal}", Name = $"Terminal {terminal}" });
resources.AddRange(Gates(gatesPerTerminal, terminal, $"T{terminal}"));
}
return resources;
}
///
/// A whole board: the resources, the days from to around today,
/// and the flights of every gate in between.
///
public static BitResourceTimelineConfig Airport(List resources, int fromDay = -7, int toDay = 7, bool labeled = false)
{
// Today is read once, so a call running across midnight gives the range and the flights the same days.
var today = DateTime.Today;
var start = new DateTimeOffset(today.AddDays(fromDay));
var end = new DateTimeOffset(today.AddDays(toDay));
return new()
{
Resources = resources,
StartDate = start,
EndDate = end,
Allocations = Flights(resources, start, end, labeled),
};
}
///
/// The turnarounds overlapping .. at every resource that is not a group
/// row: 45 minutes to 3 hours each, 30 minutes to 2 hours apart. With each carries its
/// flight number above it.
///
public static List Flights(IReadOnlyList resources, DateTimeOffset from, DateTimeOffset to, bool labeled = false)
{
var groups = resources.Where(r => r.ParentId is not null).Select(r => r.ParentId).ToHashSet();
// Counted in local calendar days rather than elapsed 24 hours, which a daylight saving change in between would
// throw off by one: the day the window starts on, through the day it ends on unless it ends at its midnight.
// Today is read once, so a call running across midnight counts the days and lays them out from the same one.
var today = DateTime.Today;
var localTo = to.LocalDateTime;
var firstDay = (int)(from.LocalDateTime.Date - today).TotalDays;
var lastDay = (int)(localTo.Date - today).TotalDays + (localTo.TimeOfDay > TimeSpan.Zero ? 1 : 0);
var flights = new List();
for (var r = 0; r < resources.Count; r++)
{
var gate = resources[r];
if (groups.Contains(gate.Id)) continue;
for (var day = firstDay; day < lastDay; day++)
{
var dayStart = new DateTimeOffset(today.AddDays(day));
var dayEnd = new DateTimeOffset(today.AddDays(day + 1));
// Seeded by the gate and the calendar date, so a day reads the same whichever window asks for it, and
// whenever it is asked for (a fetch after midnight still finds the flights the earlier ones loaded).
var random = new Random(unchecked(r * 100_003 + dayStart.Year * 10_000 + dayStart.Month * 100 + dayStart.Day));
var start = dayStart.AddMinutes(random.Next(0, 12) * 15);
for (var n = 0; ; n++)
{
var end = start.AddMinutes(random.Next(3, 13) * 15);
var flightNumber = $"{Airlines[random.Next(Airlines.Length)]}{random.Next(100, 1000)}";
if (end > dayEnd) break;
if (end > from && start < to)
{
flights.Add(new()
{
Id = $"{gate.Id}-{dayStart.ToString("yyyyMMdd", CultureInfo.InvariantCulture)}-{n}",
ResourceId = gate.Id,
StartTime = start,
EndTime = end,
TextAbove = labeled ? flightNumber : null,
});
}
start = end.AddMinutes(random.Next(2, 9) * 15);
}
}
}
return flights;
}
// An inline SVG as a data URI, so the icons need no image files.
private static string Svg(string svg) => "data:image/svg+xml," + Uri.EscapeDataString(svg);
}
```
\n**Large data**:
```razor
/// The sample data of the BitResourceTimeline demo: the gates of an airport and the turnarounds of the flights parked
/// at them, laid out around today so the current time is always on the timeline. Every day of every gate is generated
/// from a seed of its own, so the same arguments always return the same flights - and a window asked for on demand
/// returns exactly the flights a full load would have had there.
///
public static class BitResourceTimelineDemoData
{
private static readonly string[] Airlines = ["LH", "BA", "AF", "KL", "EK", "QR", "TK", "IB"];
public static readonly string WarningIcon = Svg("");
public static readonly string CheckIcon = Svg("");
public static readonly string StarIcon = Svg("");
public static readonly string LockIcon = Svg("");
public static readonly string PlaneIcon = Svg("");
/// The local midnight of today, which every example lays its bars out around.
public static DateTimeOffset Today => new(DateTime.Today);
///
/// The local midnight days from today, with the offset in force on that day - which a
/// daylight saving change in between makes differ from the one carries.
///
public static DateTimeOffset Day(int days) => new(DateTime.Today.AddDays(days));
/// Gates named Gate A1, Gate A2, ... - optionally the children of a group row.
public static List Gates(int count, string terminal = "A", string? parentId = null) =>
[.. Enumerable.Range(1, count).Select(n => new BitResourceTimelineResource { Id = $"{terminal}{n}", Name = $"Gate {terminal}{n}", ParentId = parentId })];
/// Terminal A, B, ... as group rows, each holding gates.
public static List Terminals(int terminals, int gatesPerTerminal)
{
var resources = new List();
for (var t = 0; t < terminals; t++)
{
var terminal = ((char)('A' + t)).ToString();
resources.Add(new() { Id = $"T{terminal}", Name = $"Terminal {terminal}" });
resources.AddRange(Gates(gatesPerTerminal, terminal, $"T{terminal}"));
}
return resources;
}
///
/// A whole board: the resources, the days from to around today,
/// and the flights of every gate in between.
///
public static BitResourceTimelineConfig Airport(List resources, int fromDay = -7, int toDay = 7, bool labeled = false)
{
// Today is read once, so a call running across midnight gives the range and the flights the same days.
var today = DateTime.Today;
var start = new DateTimeOffset(today.AddDays(fromDay));
var end = new DateTimeOffset(today.AddDays(toDay));
return new()
{
Resources = resources,
StartDate = start,
EndDate = end,
Allocations = Flights(resources, start, end, labeled),
};
}
///
/// The turnarounds overlapping .. at every resource that is not a group
/// row: 45 minutes to 3 hours each, 30 minutes to 2 hours apart. With each carries its
/// flight number above it.
///
public static List Flights(IReadOnlyList resources, DateTimeOffset from, DateTimeOffset to, bool labeled = false)
{
var groups = resources.Where(r => r.ParentId is not null).Select(r => r.ParentId).ToHashSet();
// Counted in local calendar days rather than elapsed 24 hours, which a daylight saving change in between would
// throw off by one: the day the window starts on, through the day it ends on unless it ends at its midnight.
// Today is read once, so a call running across midnight counts the days and lays them out from the same one.
var today = DateTime.Today;
var localTo = to.LocalDateTime;
var firstDay = (int)(from.LocalDateTime.Date - today).TotalDays;
var lastDay = (int)(localTo.Date - today).TotalDays + (localTo.TimeOfDay > TimeSpan.Zero ? 1 : 0);
var flights = new List();
for (var r = 0; r < resources.Count; r++)
{
var gate = resources[r];
if (groups.Contains(gate.Id)) continue;
for (var day = firstDay; day < lastDay; day++)
{
var dayStart = new DateTimeOffset(today.AddDays(day));
var dayEnd = new DateTimeOffset(today.AddDays(day + 1));
// Seeded by the gate and the calendar date, so a day reads the same whichever window asks for it, and
// whenever it is asked for (a fetch after midnight still finds the flights the earlier ones loaded).
var random = new Random(unchecked(r * 100_003 + dayStart.Year * 10_000 + dayStart.Month * 100 + dayStart.Day));
var start = dayStart.AddMinutes(random.Next(0, 12) * 15);
for (var n = 0; ; n++)
{
var end = start.AddMinutes(random.Next(3, 13) * 15);
var flightNumber = $"{Airlines[random.Next(Airlines.Length)]}{random.Next(100, 1000)}";
if (end > dayEnd) break;
if (end > from && start < to)
{
flights.Add(new()
{
Id = $"{gate.Id}-{dayStart.ToString("yyyyMMdd", CultureInfo.InvariantCulture)}-{n}",
ResourceId = gate.Id,
StartTime = start,
EndTime = end,
TextAbove = labeled ? flightNumber : null,
});
}
start = end.AddMinutes(random.Next(2, 9) * 15);
}
}
}
return flights;
}
// An inline SVG as a data URI, so the icons need no image files.
private static string Svg(string svg) => "data:image/svg+xml," + Uri.EscapeDataString(svg);
}
```
\n**On-demand loading**:
```razor
> LoadWindowAsync(BitResourceTimelineWindow window)
{
var request = ++windowRequests;
var range = $"{window.Start.ToLocalTime():d MMM HH:mm} - {window.End.ToLocalTime():d MMM HH:mm}";
windowedStatus = $"Request {request}: loading {range}";
StateHasChanged();
await Task.Delay(400); // simulate a query of the server
var flights = BitResourceTimelineDemoData.Flights(windowedConfig.Resources, window.Start, window.End, labeled: true);
if (request == windowRequests)
{
windowedStatus = $"Request {request}: {range}, {flights.Count:N0} flights";
StateHasChanged();
}
return flights;
}
```
`BitResourceTimelineDemoData.cs`:
```csharp
using System.Globalization;
namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Extras.ResourceTimeline;
///
/// The sample data of the BitResourceTimeline demo: the gates of an airport and the turnarounds of the flights parked
/// at them, laid out around today so the current time is always on the timeline. Every day of every gate is generated
/// from a seed of its own, so the same arguments always return the same flights - and a window asked for on demand
/// returns exactly the flights a full load would have had there.
///
public static class BitResourceTimelineDemoData
{
private static readonly string[] Airlines = ["LH", "BA", "AF", "KL", "EK", "QR", "TK", "IB"];
public static readonly string WarningIcon = Svg("");
public static readonly string CheckIcon = Svg("");
public static readonly string StarIcon = Svg("");
public static readonly string LockIcon = Svg("");
public static readonly string PlaneIcon = Svg("");
/// The local midnight of today, which every example lays its bars out around.
public static DateTimeOffset Today => new(DateTime.Today);
///
/// The local midnight days from today, with the offset in force on that day - which a
/// daylight saving change in between makes differ from the one carries.
///
public static DateTimeOffset Day(int days) => new(DateTime.Today.AddDays(days));
/// Gates named Gate A1, Gate A2, ... - optionally the children of a group row.
public static List Gates(int count, string terminal = "A", string? parentId = null) =>
[.. Enumerable.Range(1, count).Select(n => new BitResourceTimelineResource { Id = $"{terminal}{n}", Name = $"Gate {terminal}{n}", ParentId = parentId })];
/// Terminal A, B, ... as group rows, each holding gates.
public static List Terminals(int terminals, int gatesPerTerminal)
{
var resources = new List();
for (var t = 0; t < terminals; t++)
{
var terminal = ((char)('A' + t)).ToString();
resources.Add(new() { Id = $"T{terminal}", Name = $"Terminal {terminal}" });
resources.AddRange(Gates(gatesPerTerminal, terminal, $"T{terminal}"));
}
return resources;
}
///
/// A whole board: the resources, the days from to around today,
/// and the flights of every gate in between.
///
public static BitResourceTimelineConfig Airport(List resources, int fromDay = -7, int toDay = 7, bool labeled = false)
{
// Today is read once, so a call running across midnight gives the range and the flights the same days.
var today = DateTime.Today;
var start = new DateTimeOffset(today.AddDays(fromDay));
var end = new DateTimeOffset(today.AddDays(toDay));
return new()
{
Resources = resources,
StartDate = start,
EndDate = end,
Allocations = Flights(resources, start, end, labeled),
};
}
///
/// The turnarounds overlapping .. at every resource that is not a group
/// row: 45 minutes to 3 hours each, 30 minutes to 2 hours apart. With each carries its
/// flight number above it.
///
public static List Flights(IReadOnlyList resources, DateTimeOffset from, DateTimeOffset to, bool labeled = false)
{
var groups = resources.Where(r => r.ParentId is not null).Select(r => r.ParentId).ToHashSet();
// Counted in local calendar days rather than elapsed 24 hours, which a daylight saving change in between would
// throw off by one: the day the window starts on, through the day it ends on unless it ends at its midnight.
// Today is read once, so a call running across midnight counts the days and lays them out from the same one.
var today = DateTime.Today;
var localTo = to.LocalDateTime;
var firstDay = (int)(from.LocalDateTime.Date - today).TotalDays;
var lastDay = (int)(localTo.Date - today).TotalDays + (localTo.TimeOfDay > TimeSpan.Zero ? 1 : 0);
var flights = new List();
for (var r = 0; r < resources.Count; r++)
{
var gate = resources[r];
if (groups.Contains(gate.Id)) continue;
for (var day = firstDay; day < lastDay; day++)
{
var dayStart = new DateTimeOffset(today.AddDays(day));
var dayEnd = new DateTimeOffset(today.AddDays(day + 1));
// Seeded by the gate and the calendar date, so a day reads the same whichever window asks for it, and
// whenever it is asked for (a fetch after midnight still finds the flights the earlier ones loaded).
var random = new Random(unchecked(r * 100_003 + dayStart.Year * 10_000 + dayStart.Month * 100 + dayStart.Day));
var start = dayStart.AddMinutes(random.Next(0, 12) * 15);
for (var n = 0; ; n++)
{
var end = start.AddMinutes(random.Next(3, 13) * 15);
var flightNumber = $"{Airlines[random.Next(Airlines.Length)]}{random.Next(100, 1000)}";
if (end > dayEnd) break;
if (end > from && start < to)
{
flights.Add(new()
{
Id = $"{gate.Id}-{dayStart.ToString("yyyyMMdd", CultureInfo.InvariantCulture)}-{n}",
ResourceId = gate.Id,
StartTime = start,
EndTime = end,
TextAbove = labeled ? flightNumber : null,
});
}
start = end.AddMinutes(random.Next(2, 9) * 15);
}
}
}
return flights;
}
// An inline SVG as a data URI, so the icons need no image files.
private static string Svg(string svg) => "data:image/svg+xml," + Uri.EscapeDataString(svg);
}
```
\n**Disabled**:
```razor
/// The sample data of the BitResourceTimeline demo: the gates of an airport and the turnarounds of the flights parked
/// at them, laid out around today so the current time is always on the timeline. Every day of every gate is generated
/// from a seed of its own, so the same arguments always return the same flights - and a window asked for on demand
/// returns exactly the flights a full load would have had there.
///
public static class BitResourceTimelineDemoData
{
private static readonly string[] Airlines = ["LH", "BA", "AF", "KL", "EK", "QR", "TK", "IB"];
public static readonly string WarningIcon = Svg("");
public static readonly string CheckIcon = Svg("");
public static readonly string StarIcon = Svg("");
public static readonly string LockIcon = Svg("");
public static readonly string PlaneIcon = Svg("");
/// The local midnight of today, which every example lays its bars out around.
public static DateTimeOffset Today => new(DateTime.Today);
///
/// The local midnight days from today, with the offset in force on that day - which a
/// daylight saving change in between makes differ from the one carries.
///
public static DateTimeOffset Day(int days) => new(DateTime.Today.AddDays(days));
/// Gates named Gate A1, Gate A2, ... - optionally the children of a group row.
public static List Gates(int count, string terminal = "A", string? parentId = null) =>
[.. Enumerable.Range(1, count).Select(n => new BitResourceTimelineResource { Id = $"{terminal}{n}", Name = $"Gate {terminal}{n}", ParentId = parentId })];
/// Terminal A, B, ... as group rows, each holding gates.
public static List Terminals(int terminals, int gatesPerTerminal)
{
var resources = new List();
for (var t = 0; t < terminals; t++)
{
var terminal = ((char)('A' + t)).ToString();
resources.Add(new() { Id = $"T{terminal}", Name = $"Terminal {terminal}" });
resources.AddRange(Gates(gatesPerTerminal, terminal, $"T{terminal}"));
}
return resources;
}
///
/// A whole board: the resources, the days from to around today,
/// and the flights of every gate in between.
///
public static BitResourceTimelineConfig Airport(List resources, int fromDay = -7, int toDay = 7, bool labeled = false)
{
// Today is read once, so a call running across midnight gives the range and the flights the same days.
var today = DateTime.Today;
var start = new DateTimeOffset(today.AddDays(fromDay));
var end = new DateTimeOffset(today.AddDays(toDay));
return new()
{
Resources = resources,
StartDate = start,
EndDate = end,
Allocations = Flights(resources, start, end, labeled),
};
}
///
/// The turnarounds overlapping .. at every resource that is not a group
/// row: 45 minutes to 3 hours each, 30 minutes to 2 hours apart. With each carries its
/// flight number above it.
///
public static List Flights(IReadOnlyList resources, DateTimeOffset from, DateTimeOffset to, bool labeled = false)
{
var groups = resources.Where(r => r.ParentId is not null).Select(r => r.ParentId).ToHashSet();
// Counted in local calendar days rather than elapsed 24 hours, which a daylight saving change in between would
// throw off by one: the day the window starts on, through the day it ends on unless it ends at its midnight.
// Today is read once, so a call running across midnight counts the days and lays them out from the same one.
var today = DateTime.Today;
var localTo = to.LocalDateTime;
var firstDay = (int)(from.LocalDateTime.Date - today).TotalDays;
var lastDay = (int)(localTo.Date - today).TotalDays + (localTo.TimeOfDay > TimeSpan.Zero ? 1 : 0);
var flights = new List();
for (var r = 0; r < resources.Count; r++)
{
var gate = resources[r];
if (groups.Contains(gate.Id)) continue;
for (var day = firstDay; day < lastDay; day++)
{
var dayStart = new DateTimeOffset(today.AddDays(day));
var dayEnd = new DateTimeOffset(today.AddDays(day + 1));
// Seeded by the gate and the calendar date, so a day reads the same whichever window asks for it, and
// whenever it is asked for (a fetch after midnight still finds the flights the earlier ones loaded).
var random = new Random(unchecked(r * 100_003 + dayStart.Year * 10_000 + dayStart.Month * 100 + dayStart.Day));
var start = dayStart.AddMinutes(random.Next(0, 12) * 15);
for (var n = 0; ; n++)
{
var end = start.AddMinutes(random.Next(3, 13) * 15);
var flightNumber = $"{Airlines[random.Next(Airlines.Length)]}{random.Next(100, 1000)}";
if (end > dayEnd) break;
if (end > from && start < to)
{
flights.Add(new()
{
Id = $"{gate.Id}-{dayStart.ToString("yyyyMMdd", CultureInfo.InvariantCulture)}-{n}",
ResourceId = gate.Id,
StartTime = start,
EndTime = end,
TextAbove = labeled ? flightNumber : null,
});
}
start = end.AddMinutes(random.Next(2, 9) * 15);
}
}
}
return flights;
}
// An inline SVG as a data URI, so the icons need no image files.
private static string Svg(string svg) => "data:image/svg+xml," + Uri.EscapeDataString(svg);
}
```
\n**Texts**:
```razor
```
```csharp
private BitResourceTimelineConfig textsConfig = BitResourceTimelineDemoData.Airport(BitResourceTimelineDemoData.Gates(5), labeled: true);
private readonly BitResourceTimelineOptions textsOptions = new() { AutoScrollToNow = true, Locale = "de-DE" };
private readonly BitResourceTimelineTexts germanTexts = new()
{
AriaLabel = "Belegungsplan der Gates",
RoleDescription = "Ressourcen-Zeitachse",
Loading = "Wird geladen…",
ExpandGroupFormat = "{0} aufklappen",
CollapseGroupFormat = "{0} zuklappen",
ResizeResourceColumn = "Breite der Ressourcenspalte ändern",
BarLocked = "Balken ist gesperrt",
SelectionCleared = "Auswahl aufgehoben",
EmptyRowFormat = "{0}, keine Belegungen",
FocusedBarFormat = "{0}: {1} bis {2}",
FocusedLabeledBarFormat = "{0}: {1}, {2} bis {3}",
SelectedBarFormat = "{0}, ausgewählt",
ResizedFormat = "Größe geändert: {0} bis {1}",
MovedFormat = "Verschoben: {0} bis {1}",
MovedToResourceFormat = "Verschoben nach {0}: {1} bis {2}",
EditRefused = "Änderung abgelehnt",
EditRefusedOverlap = "Änderung abgelehnt: überschneidet eine andere Belegung",
GroupCollapsed = "Gruppe zugeklappt",
GroupExpanded = "Gruppe aufgeklappt",
CopiedFormat = "{0} kopiert",
PastedFormat = "{0} eingefügt",
DeletedFormat = "{0} gelöscht",
DeleteRefused = "Löschen abgelehnt",
TimeRangeFormat = "{0} bis {1}",
HiddenAllocationFormat = "{0} ausgeblendete Belegung",
HiddenAllocationsFormat = "{0} ausgeblendete Belegungen",
HiddenAllocationItemFormat = "{0} · {1}",
MoreHiddenAllocationsFormat = "… und {0} weitere",
};
```
`BitResourceTimelineDemoData.cs`:
```csharp
using System.Globalization;
namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Extras.ResourceTimeline;
///
/// The sample data of the BitResourceTimeline demo: the gates of an airport and the turnarounds of the flights parked
/// at them, laid out around today so the current time is always on the timeline. Every day of every gate is generated
/// from a seed of its own, so the same arguments always return the same flights - and a window asked for on demand
/// returns exactly the flights a full load would have had there.
///
public static class BitResourceTimelineDemoData
{
private static readonly string[] Airlines = ["LH", "BA", "AF", "KL", "EK", "QR", "TK", "IB"];
public static readonly string WarningIcon = Svg("");
public static readonly string CheckIcon = Svg("");
public static readonly string StarIcon = Svg("");
public static readonly string LockIcon = Svg("");
public static readonly string PlaneIcon = Svg("");
/// The local midnight of today, which every example lays its bars out around.
public static DateTimeOffset Today => new(DateTime.Today);
///
/// The local midnight days from today, with the offset in force on that day - which a
/// daylight saving change in between makes differ from the one carries.
///
public static DateTimeOffset Day(int days) => new(DateTime.Today.AddDays(days));
/// Gates named Gate A1, Gate A2, ... - optionally the children of a group row.
public static List Gates(int count, string terminal = "A", string? parentId = null) =>
[.. Enumerable.Range(1, count).Select(n => new BitResourceTimelineResource { Id = $"{terminal}{n}", Name = $"Gate {terminal}{n}", ParentId = parentId })];
/// Terminal A, B, ... as group rows, each holding gates.
public static List Terminals(int terminals, int gatesPerTerminal)
{
var resources = new List();
for (var t = 0; t < terminals; t++)
{
var terminal = ((char)('A' + t)).ToString();
resources.Add(new() { Id = $"T{terminal}", Name = $"Terminal {terminal}" });
resources.AddRange(Gates(gatesPerTerminal, terminal, $"T{terminal}"));
}
return resources;
}
///
/// A whole board: the resources, the days from to around today,
/// and the flights of every gate in between.
///
public static BitResourceTimelineConfig Airport(List resources, int fromDay = -7, int toDay = 7, bool labeled = false)
{
// Today is read once, so a call running across midnight gives the range and the flights the same days.
var today = DateTime.Today;
var start = new DateTimeOffset(today.AddDays(fromDay));
var end = new DateTimeOffset(today.AddDays(toDay));
return new()
{
Resources = resources,
StartDate = start,
EndDate = end,
Allocations = Flights(resources, start, end, labeled),
};
}
///
/// The turnarounds overlapping .. at every resource that is not a group
/// row: 45 minutes to 3 hours each, 30 minutes to 2 hours apart. With each carries its
/// flight number above it.
///
public static List Flights(IReadOnlyList resources, DateTimeOffset from, DateTimeOffset to, bool labeled = false)
{
var groups = resources.Where(r => r.ParentId is not null).Select(r => r.ParentId).ToHashSet();
// Counted in local calendar days rather than elapsed 24 hours, which a daylight saving change in between would
// throw off by one: the day the window starts on, through the day it ends on unless it ends at its midnight.
// Today is read once, so a call running across midnight counts the days and lays them out from the same one.
var today = DateTime.Today;
var localTo = to.LocalDateTime;
var firstDay = (int)(from.LocalDateTime.Date - today).TotalDays;
var lastDay = (int)(localTo.Date - today).TotalDays + (localTo.TimeOfDay > TimeSpan.Zero ? 1 : 0);
var flights = new List();
for (var r = 0; r < resources.Count; r++)
{
var gate = resources[r];
if (groups.Contains(gate.Id)) continue;
for (var day = firstDay; day < lastDay; day++)
{
var dayStart = new DateTimeOffset(today.AddDays(day));
var dayEnd = new DateTimeOffset(today.AddDays(day + 1));
// Seeded by the gate and the calendar date, so a day reads the same whichever window asks for it, and
// whenever it is asked for (a fetch after midnight still finds the flights the earlier ones loaded).
var random = new Random(unchecked(r * 100_003 + dayStart.Year * 10_000 + dayStart.Month * 100 + dayStart.Day));
var start = dayStart.AddMinutes(random.Next(0, 12) * 15);
for (var n = 0; ; n++)
{
var end = start.AddMinutes(random.Next(3, 13) * 15);
var flightNumber = $"{Airlines[random.Next(Airlines.Length)]}{random.Next(100, 1000)}";
if (end > dayEnd) break;
if (end > from && start < to)
{
flights.Add(new()
{
Id = $"{gate.Id}-{dayStart.ToString("yyyyMMdd", CultureInfo.InvariantCulture)}-{n}",
ResourceId = gate.Id,
StartTime = start,
EndTime = end,
TextAbove = labeled ? flightNumber : null,
});
}
start = end.AddMinutes(random.Next(2, 9) * 15);
}
}
}
return flights;
}
// An inline SVG as a data URI, so the icons need no image files.
private static string Svg(string svg) => "data:image/svg+xml," + Uri.EscapeDataString(svg);
}
```
\n**Style & Class**:
```razor
Gates
@row.Resource.Name
```
```csharp
private readonly BitResourceTimelineConfig styleConfig = BitResourceTimelineDemoData.Airport(BitResourceTimelineDemoData.Gates(3), fromDay: 0, toDay: 3, labeled: true);
private readonly BitResourceTimelineOptions colorsOptions = new()
{
Colors = new()
{
Bar = "#9c36b5",
BarSelected = "#5f3dc4",
BarSelectedBorder = "#e8590c",
Now = "#e8590c",
Grid = "rgba(156, 54, 181, 0.2)",
},
};
```
`BitResourceTimelineDemoData.cs`:
```csharp
using System.Globalization;
namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Extras.ResourceTimeline;
///
/// The sample data of the BitResourceTimeline demo: the gates of an airport and the turnarounds of the flights parked
/// at them, laid out around today so the current time is always on the timeline. Every day of every gate is generated
/// from a seed of its own, so the same arguments always return the same flights - and a window asked for on demand
/// returns exactly the flights a full load would have had there.
///
public static class BitResourceTimelineDemoData
{
private static readonly string[] Airlines = ["LH", "BA", "AF", "KL", "EK", "QR", "TK", "IB"];
public static readonly string WarningIcon = Svg("");
public static readonly string CheckIcon = Svg("");
public static readonly string StarIcon = Svg("");
public static readonly string LockIcon = Svg("");
public static readonly string PlaneIcon = Svg("");
/// The local midnight of today, which every example lays its bars out around.
public static DateTimeOffset Today => new(DateTime.Today);
///
/// The local midnight days from today, with the offset in force on that day - which a
/// daylight saving change in between makes differ from the one carries.
///
public static DateTimeOffset Day(int days) => new(DateTime.Today.AddDays(days));
/// Gates named Gate A1, Gate A2, ... - optionally the children of a group row.
public static List Gates(int count, string terminal = "A", string? parentId = null) =>
[.. Enumerable.Range(1, count).Select(n => new BitResourceTimelineResource { Id = $"{terminal}{n}", Name = $"Gate {terminal}{n}", ParentId = parentId })];
/// Terminal A, B, ... as group rows, each holding gates.
public static List Terminals(int terminals, int gatesPerTerminal)
{
var resources = new List();
for (var t = 0; t < terminals; t++)
{
var terminal = ((char)('A' + t)).ToString();
resources.Add(new() { Id = $"T{terminal}", Name = $"Terminal {terminal}" });
resources.AddRange(Gates(gatesPerTerminal, terminal, $"T{terminal}"));
}
return resources;
}
///
/// A whole board: the resources, the days from to around today,
/// and the flights of every gate in between.
///
public static BitResourceTimelineConfig Airport(List resources, int fromDay = -7, int toDay = 7, bool labeled = false)
{
// Today is read once, so a call running across midnight gives the range and the flights the same days.
var today = DateTime.Today;
var start = new DateTimeOffset(today.AddDays(fromDay));
var end = new DateTimeOffset(today.AddDays(toDay));
return new()
{
Resources = resources,
StartDate = start,
EndDate = end,
Allocations = Flights(resources, start, end, labeled),
};
}
///
/// The turnarounds overlapping .. at every resource that is not a group
/// row: 45 minutes to 3 hours each, 30 minutes to 2 hours apart. With each carries its
/// flight number above it.
///
public static List Flights(IReadOnlyList resources, DateTimeOffset from, DateTimeOffset to, bool labeled = false)
{
var groups = resources.Where(r => r.ParentId is not null).Select(r => r.ParentId).ToHashSet();
// Counted in local calendar days rather than elapsed 24 hours, which a daylight saving change in between would
// throw off by one: the day the window starts on, through the day it ends on unless it ends at its midnight.
// Today is read once, so a call running across midnight counts the days and lays them out from the same one.
var today = DateTime.Today;
var localTo = to.LocalDateTime;
var firstDay = (int)(from.LocalDateTime.Date - today).TotalDays;
var lastDay = (int)(localTo.Date - today).TotalDays + (localTo.TimeOfDay > TimeSpan.Zero ? 1 : 0);
var flights = new List();
for (var r = 0; r < resources.Count; r++)
{
var gate = resources[r];
if (groups.Contains(gate.Id)) continue;
for (var day = firstDay; day < lastDay; day++)
{
var dayStart = new DateTimeOffset(today.AddDays(day));
var dayEnd = new DateTimeOffset(today.AddDays(day + 1));
// Seeded by the gate and the calendar date, so a day reads the same whichever window asks for it, and
// whenever it is asked for (a fetch after midnight still finds the flights the earlier ones loaded).
var random = new Random(unchecked(r * 100_003 + dayStart.Year * 10_000 + dayStart.Month * 100 + dayStart.Day));
var start = dayStart.AddMinutes(random.Next(0, 12) * 15);
for (var n = 0; ; n++)
{
var end = start.AddMinutes(random.Next(3, 13) * 15);
var flightNumber = $"{Airlines[random.Next(Airlines.Length)]}{random.Next(100, 1000)}";
if (end > dayEnd) break;
if (end > from && start < to)
{
flights.Add(new()
{
Id = $"{gate.Id}-{dayStart.ToString("yyyyMMdd", CultureInfo.InvariantCulture)}-{n}",
ResourceId = gate.Id,
StartTime = start,
EndTime = end,
TextAbove = labeled ? flightNumber : null,
});
}
start = end.AddMinutes(random.Next(2, 9) * 15);
}
}
}
return flights;
}
// An inline SVG as a data URI, so the icons need no image files.
private static string Svg(string svg) => "data:image/svg+xml," + Uri.EscapeDataString(svg);
}
```
@NavigationText
```
```csharp
private BitResourceTimeline navigationRef = default!;
private readonly BitResourceTimelineConfig navigationConfig = BitResourceTimelineDemoData.Airport(BitResourceTimelineDemoData.Gates(6), fromDay: -14, toDay: 14, labeled: true);
private bool panToDayStart;
private BitResourceTimelineOptions navigationOptions = CreateNavigationOptions(false);
private BitResourceTimelineView? navigationView;
private static BitResourceTimelineOptions CreateNavigationOptions(bool panToDayStart) => new()
{
AutoScrollToNow = true,
MinPixelsPerHour = 2,
MaxPixelsPerHour = 240,
PanToDayStart = panToDayStart,
};
private string NavigationText => navigationView is null
? "Scroll or zoom the timeline"
: $"{navigationView.Start.ToLocalTime():ddd d MMM HH:mm} - {navigationView.End.ToLocalTime():ddd d MMM HH:mm} · {navigationView.PixelsPerHour:0.#} px/h";
```
`BitResourceTimelineDemoData.cs`:
```csharp
using System.Globalization;
namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Extras.ResourceTimeline;
/// Selected: @(selectedBars.Length == 0 ? "nothing" : string.Join(", ", selectedBars.Select(b => b.TextAbove)))
Click: @clickText
Double-click: @doubleClickText
Right-click: @contextMenuText
```
```csharp
private BitResourceTimeline selectionRef = default!;
private readonly BitResourceTimelineConfig selectionConfig = BitResourceTimelineDemoData.Airport(BitResourceTimelineDemoData.Gates(12), labeled: true);
private readonly BitResourceTimelineOptions selectionOptions = new() { AutoScrollToNow = true };
private BitResourceTimelineAllocation[] selectedBars = [];
private string? clickText;
private string? doubleClickText;
private string? contextMenuText;
private async Task SelectNextFlights()
{
var now = DateTimeOffset.Now;
var next = selectionConfig.Allocations.Where(f => f.StartTime > now).OrderBy(f => f.StartTime).Take(3).Select(f => f.Id).ToArray();
await selectionRef.SelectAsync(next);
}
private async Task ScrollToSelection()
{
var selected = await selectionRef.GetSelectedBarsAsync();
if (selected.Count > 0)
{
await selectionRef.ScrollToAllocationAsync(selected[0].Id);
}
}
private static string DescribePointer(BitResourceTimelinePointerArgs args)
{
List
@foreach (var line in editLog)
{
```
```csharp
private readonly BitResourceTimelineConfig editingConfig = CreateEditingConfig();
private bool allowResourceChange = true;
private BitResourceTimelineOptions editingOptions = CreateEditingOptions(true);
private readonly List@line
}
Gates
flights in 2 weeks
@row.Resource.Name
@FlightsOf(row.Resource.Id)
}
@flight.TextAbove
Gate @flight.ResourceId
@flight.StartTime.UtcDateTime.ToString("ddd HH:mm") - @flight.EndTime.UtcDateTime.ToString("HH:mm") UTC
ResourceAxisWidth: @resourceAxisWidth px
```
```csharp
private readonly BitResourceTimelineConfig columnConfig = CreateColumnConfig();
private int resourceAxisWidth = 200;
private bool resourceAxisResizable = true;
private BitResourceTimelineOptions columnOptions = CreateColumnOptions(200, true);
private static BitResourceTimelineConfig CreateColumnConfig()
{
var config = BitResourceTimelineDemoData.Airport(BitResourceTimelineDemoData.Gates(6), labeled: true);
foreach (var gate in config.Resources)
{
gate.Name = $"{gate.Name} - long-haul departures";
}
return config;
}
// The width a resize committed comes back through OnResourceAxisWidthChanged; passing it back keeps it.
private void UpdateColumnOptions(int width)
{
resourceAxisWidth = width;
columnOptions = CreateColumnOptions(width, resourceAxisResizable);
}
private static BitResourceTimelineOptions CreateColumnOptions(int width, bool resizable) => new()
{
AutoScrollToNow = true,
ResourceAxisWidth = width,
ResourceAxisMinWidth = 100,
ResourceAxisMaxWidth = 360,
ResourceAxisResizable = resizable,
};
```
`BitResourceTimelineDemoData.cs`:
```csharp
using System.Globalization;
namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Extras.ResourceTimeline;
/// @LargeConfig.Allocations.Count.ToString("N0") flights on @LargeConfig.Resources.Count resources@(largeStatus is null ? null : $" · {largeStatus}")
```
```csharp
private BitResourceTimeline largeRef = default!;
private BitResourceTimelineConfig? largeConfig;
private readonly BitResourceTimelineOptions largeOptions = new() { AutoScrollToNow = true, MinPixelsPerHour = 1 };
private string? largeStatus;
// About 40,000 flights of 160 gates over five weeks, built the first time the example renders.
private BitResourceTimelineConfig LargeConfig => largeConfig ??= BitResourceTimelineDemoData.Airport(BitResourceTimelineDemoData.Terminals(8, 20), fromDay: -14, toDay: 21);
private async Task DelayFlights()
{
var watch = Stopwatch.StartNew();
var now = DateTimeOffset.Now;
var delayed = LargeConfig.Allocations.Where(f => f.StartTime > now && f.EndBar is null).Take(500).ToList();
foreach (var flight in delayed)
{
flight.Color = "#e8590c";
flight.EndBar = new() { Duration = TimeSpan.FromMinutes(45), Color = "#fa5252" };
}
// Merged by id, without reloading the rest or losing the selection.
await largeRef.UpsertAllocationsAsync(delayed);
largeStatus = $"{delayed.Count} flights delayed in {watch.ElapsedMilliseconds} ms";
}
private async Task CancelFlights()
{
var watch = Stopwatch.StartNew();
var now = DateTimeOffset.Now;
var cancelled = LargeConfig.Allocations.Where(f => f.StartTime > now).Take(500).Select(f => f.Id).ToHashSet();
// RemoveAllocationsAsync leaves Config.Allocations alone, so the app keeps it in step itself.
LargeConfig.Allocations.RemoveAll(f => cancelled.Contains(f.Id));
await largeRef.RemoveAllocationsAsync([.. cancelled]);
largeStatus = $"{cancelled.Count} flights cancelled in {watch.ElapsedMilliseconds} ms";
}
private async Task ShiftAllFlights()
{
var watch = Stopwatch.StartNew();
foreach (var flight in LargeConfig.Allocations)
{
flight.StartTime = flight.StartTime.AddHours(1);
flight.EndTime = flight.EndTime.AddHours(1);
}
// The Config was changed in place and its reference is the same, so ReloadAsync sends it again.
await largeRef.ReloadAsync();
largeStatus = $"{LargeConfig.Allocations.Count:N0} flights moved an hour later in {watch.ElapsedMilliseconds} ms";
}
```
`BitResourceTimelineDemoData.cs`:
```csharp
using System.Globalization;
namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Extras.ResourceTimeline;
/// @windowedStatus
```
```csharp
// 100 gates over eight months - some 170,000 flights, far too many to load up front. The Config holds the
// resources and the range alone; the bars come from LoadWindowAsync.
private readonly BitResourceTimelineConfig windowedConfig = new()
{
Resources = BitResourceTimelineDemoData.Terminals(5, 20),
StartDate = BitResourceTimelineDemoData.Today.AddDays(-120),
EndDate = BitResourceTimelineDemoData.Today.AddDays(120),
Allocations = [],
};
private readonly BitResourceTimelineOptions windowedOptions = new() { AutoScrollToNow = true, MinPixelsPerHour = 6 };
private int windowRequests;
private string windowedStatus = "Waiting for the first window";
// The timeline drops a window when a newer request arrives before it is applied - while this call works, or while
// its result waits for the one ahead of it - and every newer request calls this again. So a call says it is loading
// as it starts, which replaces the status of any window before it, and reports its flights only while no newer call
// has started: the status never names a window the timeline dropped.
private async TaskClicks: @disabledClicks
```
```csharp
private readonly BitResourceTimelineConfig disabledConfig = BitResourceTimelineDemoData.Airport(BitResourceTimelineDemoData.Gates(5), labeled: true);
private readonly BitResourceTimelineOptions disabledOptions = new() { AutoScrollToNow = true, Editable = true };
private bool isDisabled = true;
private int disabledClicks;
```
`BitResourceTimelineDemoData.cs`:
```csharp
using System.Globalization;
namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Extras.ResourceTimeline;
/// Style & Class (with CSS variables):
Styles:
Classes:
Options.Colors: