cats-charts 0.0.77 → 0.0.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -637,3 +637,76 @@ Feel free to submit a pull request.
637
637
  # 📄 License
638
638
 
639
639
  MIT License
640
+
641
+ ---
642
+
643
+ # Status Timeline
644
+
645
+ `StatusTimelineComponent` renders generic categorical intervals in chronological order. Segment
646
+ widths are proportional to duration, and dense datasets scroll horizontally without aggregation.
647
+
648
+ ## Import and usage
649
+
650
+ ```ts
651
+ import { StatusTimelineComponent, type StatusTimelineConfig } from 'cats-charts';
652
+
653
+ @Component({
654
+ standalone: true,
655
+ imports: [StatusTimelineComponent],
656
+ template: `
657
+ <lib-status-timeline
658
+ [config]="timelineConfig"
659
+ (chartEvent)="onTimelineEvent($event)"
660
+ />
661
+ `,
662
+ })
663
+ export class ExampleComponent {
664
+ timelineConfig: StatusTimelineConfig = {
665
+ startLabel: 'Monday',
666
+ endLabel: 'Today',
667
+ statuses: [
668
+ { value: 'ready', label: 'Ready', color: '#2f8f3a' },
669
+ { value: 'deploying', label: 'Deploying', color: '#d0a020' },
670
+ { value: 'blocked', label: 'Blocked', color: '#a43b35' },
671
+ ],
672
+ data: [
673
+ { start: '2026-08-24T00:00:00Z', end: '2026-08-25T00:00:00Z', status: 'ready' },
674
+ {
675
+ start: '2026-08-25T00:00:00Z',
676
+ end: '2026-08-25T06:00:00Z',
677
+ status: 'deploying',
678
+ label: 'Release deployment',
679
+ },
680
+ { start: '2026-08-25T06:00:00Z', end: '2026-08-26T00:00:00Z', status: 'ready' },
681
+ ],
682
+ };
683
+ }
684
+ ```
685
+
686
+ ## Configuration
687
+
688
+ | Option | Description |
689
+ | --- | --- |
690
+ | `data` | Dynamic intervals with `start`, `end`, `status`, and optional `label`/`metadata` |
691
+ | `statuses` | Text and optional color for each status value |
692
+ | `showLegend`, `showTooltip` | Toggle supporting UI (both default to `true`) |
693
+ | `legendPosition` | Places the legend at `top`, `bottom`, `left`, or `right` |
694
+ | `legendLayout` | Arranges legend items `horizontal` or `vertical` |
695
+ | `legendAlign` | Aligns the legend at `start`/`left`, `center`, `end`/`right`, or `space-between` |
696
+ | `legendLabelAlign` | Aligns text within vertical legend items to `left`, `center`, or `right` |
697
+ | `legendInteractive` | Enables clicking legend labels to dim/restore matching intervals; defaults to `true` |
698
+ | `legendDimmedOpacity` | Opacity used for inactive legend items and intervals; defaults to `0.3` |
699
+ | `legendScrollable` | Enables bounded legend overflow scrolling; defaults to `true` |
700
+ | `legendMaxWidth`, `legendMaxHeight` | Optional scroll viewport limits |
701
+ | `legendNote` | Optional explanatory text displayed beside the legend |
702
+ | `startLabel`, `endLabel` | Endpoint text; dates are derived when omitted |
703
+ | `showEndpointLabels` | Shows or hides the endpoint-label row; defaults to `true` |
704
+ | `endpointLabelsAlign` | Aligns endpoint labels at `start`/`left`, `center`, `end`/`right`, or `space-between` |
705
+ | `loading`, `loadingLabel`, `emptyLabel`, `error` | Timeline state presentation |
706
+ | `minSegmentWidth`, `segmentGap` | Dense-data and spacing controls in pixels |
707
+ | `dateFormatter`, `durationFormatter`, `tooltipFormatter` | Display customization hooks |
708
+ | `unknownStatus` | Fallback label/color for unconfigured status values |
709
+ | `ariaLabel` | Accessible name for the timeline |
710
+
711
+ The component emits `chartEvent` with `{ type: 'itemClick', item, index }`. Its appearance can be
712
+ themed with the `--status-timeline-*` CSS custom properties defined in the component stylesheet.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { EventEmitter, Output, Input, Component, Injectable, PLATFORM_ID, ViewChild, Inject, ChangeDetectionStrategy, HostListener } from '@angular/core';
2
+ import { EventEmitter, Output, Input, Component, Injectable, PLATFORM_ID, ViewChild, Inject, ChangeDetectionStrategy, HostListener, inject, APP_ID } from '@angular/core';
3
3
  import * as echarts from 'echarts/core';
4
4
  import { NgxEchartsDirective, provideEchartsCore } from 'ngx-echarts';
5
5
  import { LineChart, BarChart as BarChart$1, PieChart as PieChart$1, CustomChart, SankeyChart as SankeyChart$1, HeatmapChart, ScatterChart } from 'echarts/charts';
@@ -4105,9 +4105,213 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
4105
4105
  type: Output
4106
4106
  }] } });
4107
4107
 
4108
+ const DEFAULT_COLORS = ['#2f8f3a', '#d0a020', '#dc8a1f', '#a43b35', '#d9dde1'];
4109
+ const DEFAULT_UNKNOWN_STATUS = {
4110
+ value: '__unknown__',
4111
+ label: 'Unknown status',
4112
+ color: '#7b8491',
4113
+ };
4114
+ let statusTimelineInstanceSequence = 0;
4115
+ class StatusTimelineComponent {
4116
+ config;
4117
+ chartEvent = new EventEmitter();
4118
+ renderedItems = [];
4119
+ legendStatuses = [];
4120
+ activeIndex = null;
4121
+ timelineMinWidth = '100%';
4122
+ resolvedStartLabel = '';
4123
+ resolvedEndLabel = '';
4124
+ tooltipIdPrefix = `${inject(APP_ID).replace(/[^a-zA-Z0-9_-]/g, '-')}-status-timeline-${statusTimelineInstanceSequence++}-tooltip`;
4125
+ dimmedStatuses = new Set();
4126
+ get legendPosition() {
4127
+ return this.config.legendPosition ?? 'bottom';
4128
+ }
4129
+ get legendLayout() {
4130
+ return (this.config.legendLayout ??
4131
+ (this.legendPosition === 'left' || this.legendPosition === 'right' ? 'vertical' : 'horizontal'));
4132
+ }
4133
+ get legendScrollable() {
4134
+ return this.config.legendScrollable !== false;
4135
+ }
4136
+ get legendInteractive() {
4137
+ return this.config.legendInteractive !== false;
4138
+ }
4139
+ get legendAlign() {
4140
+ if (this.config.legendAlign === 'left')
4141
+ return 'start';
4142
+ if (this.config.legendAlign === 'right')
4143
+ return 'end';
4144
+ return this.config.legendAlign ?? 'start';
4145
+ }
4146
+ get legendLabelAlign() {
4147
+ return this.config.legendLabelAlign ?? 'left';
4148
+ }
4149
+ get legendLabelJustify() {
4150
+ if (this.legendLabelAlign === 'center')
4151
+ return 'center';
4152
+ if (this.legendLabelAlign === 'right')
4153
+ return 'flex-end';
4154
+ return 'flex-start';
4155
+ }
4156
+ get endpointLabelsAlign() {
4157
+ if (this.config.endpointLabelsAlign === 'left')
4158
+ return 'start';
4159
+ if (this.config.endpointLabelsAlign === 'right')
4160
+ return 'end';
4161
+ return this.config.endpointLabelsAlign ?? 'space-between';
4162
+ }
4163
+ get legendMaxWidth() {
4164
+ return this.config.legendMaxWidth ?? null;
4165
+ }
4166
+ get legendMaxHeight() {
4167
+ return this.config.legendMaxHeight ?? (this.legendLayout === 'vertical' ? '240px' : null);
4168
+ }
4169
+ ngOnChanges() {
4170
+ this.rebuild();
4171
+ }
4172
+ activate(index) {
4173
+ this.activeIndex = index;
4174
+ }
4175
+ deactivate(index) {
4176
+ if (this.activeIndex === index)
4177
+ this.activeIndex = null;
4178
+ }
4179
+ selectItem(item) {
4180
+ this.chartEvent.emit({
4181
+ type: 'itemClick',
4182
+ item: item.source,
4183
+ index: item.originalIndex,
4184
+ });
4185
+ }
4186
+ isStatusSelected(value) {
4187
+ return !this.dimmedStatuses.has(value);
4188
+ }
4189
+ toggleStatus(status) {
4190
+ if (!this.legendInteractive)
4191
+ return;
4192
+ if (this.dimmedStatuses.has(status.value)) {
4193
+ this.dimmedStatuses.delete(status.value);
4194
+ }
4195
+ else {
4196
+ this.dimmedStatuses.add(status.value);
4197
+ }
4198
+ this.chartEvent.emit({
4199
+ type: 'legendToggle',
4200
+ status,
4201
+ selected: this.isStatusSelected(status.value),
4202
+ });
4203
+ }
4204
+ tooltipId(index) {
4205
+ return `${this.tooltipIdPrefix}-${index}`;
4206
+ }
4207
+ trackItem(_, item) {
4208
+ return `${item.start.getTime()}-${item.end.getTime()}-${item.status.value}-${item.originalIndex}`;
4209
+ }
4210
+ trackStatus(_, status) {
4211
+ return status.value;
4212
+ }
4213
+ rebuild() {
4214
+ const config = this.config;
4215
+ if (!config) {
4216
+ this.renderedItems = [];
4217
+ this.legendStatuses = [];
4218
+ return;
4219
+ }
4220
+ const statusMap = new Map();
4221
+ this.legendStatuses = (config.statuses ?? []).map((status, index) => {
4222
+ const resolved = { ...status, color: status.color ?? DEFAULT_COLORS[index % DEFAULT_COLORS.length] };
4223
+ statusMap.set(resolved.value, resolved);
4224
+ return resolved;
4225
+ });
4226
+ const configuredStatusValues = new Set(this.legendStatuses.map((status) => status.value));
4227
+ this.dimmedStatuses = new Set([...this.dimmedStatuses].filter((value) => configuredStatusValues.has(value)));
4228
+ const unknownStatus = {
4229
+ ...DEFAULT_UNKNOWN_STATUS,
4230
+ ...config.unknownStatus,
4231
+ value: config.unknownStatus?.value ?? DEFAULT_UNKNOWN_STATUS.value,
4232
+ label: config.unknownStatus?.label ?? DEFAULT_UNKNOWN_STATUS.label,
4233
+ color: config.unknownStatus?.color ?? DEFAULT_UNKNOWN_STATUS.color,
4234
+ };
4235
+ this.renderedItems = (config.data ?? [])
4236
+ .map((item, originalIndex) => {
4237
+ const start = this.toDate(item.start);
4238
+ const end = this.toDate(item.end);
4239
+ if (!start || !end || end.getTime() <= start.getTime())
4240
+ return null;
4241
+ const status = statusMap.get(item.status) ?? {
4242
+ ...unknownStatus,
4243
+ value: item.status,
4244
+ label: config.unknownStatus?.label ?? `Unknown (${item.status})`,
4245
+ };
4246
+ const durationMs = end.getTime() - start.getTime();
4247
+ const context = { item, status, start, end, durationMs };
4248
+ const tooltip = config.tooltipFormatter?.(context) ?? this.defaultTooltip(context);
4249
+ const accessibleLabel = [item.label, status.label, tooltip].filter(Boolean).join('. ');
4250
+ const tooltipLines = tooltip
4251
+ .split(/\r?\n/)
4252
+ .map((line) => line.trim())
4253
+ .filter(Boolean);
4254
+ return {
4255
+ source: item,
4256
+ originalIndex,
4257
+ start,
4258
+ end,
4259
+ durationMs,
4260
+ status,
4261
+ tooltip,
4262
+ tooltipLines,
4263
+ accessibleLabel,
4264
+ };
4265
+ })
4266
+ .filter((item) => item !== null)
4267
+ .sort((left, right) => left.start.getTime() - right.start.getTime());
4268
+ const gap = Math.max(config.segmentGap ?? 2, 0);
4269
+ const minimum = Math.max(config.minSegmentWidth ?? 6, 1);
4270
+ const requiredWidth = this.renderedItems.length * minimum + Math.max(this.renderedItems.length - 1, 0) * gap;
4271
+ this.timelineMinWidth = `max(100%, ${requiredWidth}px)`;
4272
+ this.resolvedStartLabel = config.startLabel ?? this.formatDate(this.renderedItems[0]?.start);
4273
+ this.resolvedEndLabel = config.endLabel ?? this.formatDate(this.renderedItems.at(-1)?.end);
4274
+ this.activeIndex = null;
4275
+ }
4276
+ toDate(value) {
4277
+ const date = value instanceof Date ? new Date(value.getTime()) : new Date(value);
4278
+ return Number.isFinite(date.getTime()) ? date : null;
4279
+ }
4280
+ formatDate(date) {
4281
+ if (!date)
4282
+ return '';
4283
+ return (this.config.dateFormatter?.(date) ??
4284
+ new Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(date));
4285
+ }
4286
+ defaultTooltip(context) {
4287
+ const duration = this.config.durationFormatter?.(context.durationMs) ?? this.formatDuration(context.durationMs);
4288
+ return `${context.status.label}. Start: ${this.formatDate(context.start)}. End: ${this.formatDate(context.end)}. Duration: ${duration}`;
4289
+ }
4290
+ formatDuration(durationMs) {
4291
+ const totalMinutes = Math.max(Math.round(durationMs / 60_000), 1);
4292
+ const days = Math.floor(totalMinutes / 1_440);
4293
+ const hours = Math.floor((totalMinutes % 1_440) / 60);
4294
+ const minutes = totalMinutes % 60;
4295
+ return [days ? `${days}d` : '', hours ? `${hours}h` : '', minutes || (!days && !hours) ? `${minutes}m` : '']
4296
+ .filter(Boolean)
4297
+ .join(' ');
4298
+ }
4299
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: StatusTimelineComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4300
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: StatusTimelineComponent, isStandalone: true, selector: "lib-status-timeline", inputs: { config: "config" }, outputs: { chartEvent: "chartEvent" }, usesOnChanges: true, ngImport: i0, template: "<section\n class=\"status-timeline\"\n [style.--status-timeline-gap]=\"config.segmentGap ?? 2\"\n [style.--status-timeline-min-segment-width]=\"config.minSegmentWidth ?? 6\"\n [style.--status-timeline-dimmed-opacity]=\"config.legendDimmedOpacity ?? 0.3\"\n [class.status-timeline--legend-top]=\"legendPosition === 'top'\"\n [class.status-timeline--legend-bottom]=\"legendPosition === 'bottom'\"\n [class.status-timeline--legend-left]=\"legendPosition === 'left'\"\n [class.status-timeline--legend-right]=\"legendPosition === 'right'\"\n [attr.aria-label]=\"config.ariaLabel || 'Status timeline'\"\n>\n @if (config.loading) {\n <div class=\"status-timeline__state\" role=\"status\" aria-live=\"polite\">\n <span class=\"status-timeline__skeleton\" aria-hidden=\"true\"></span>\n <span>{{ config.loadingLabel || 'Loading timeline' }}</span>\n </div>\n } @else if (config.error) {\n <div class=\"status-timeline__state status-timeline__state--error\" role=\"alert\">\n {{ config.error }}\n </div>\n } @else if (!renderedItems.length) {\n <div class=\"status-timeline__state\" role=\"status\">\n {{ config.emptyLabel || 'No timeline data available' }}\n </div>\n } @else {\n <div class=\"status-timeline__visual\">\n <div class=\"status-timeline__scroll\" tabindex=\"0\" aria-label=\"Scrollable status intervals\">\n <div class=\"status-timeline__content\" [style.min-width]=\"timelineMinWidth\">\n <div class=\"status-timeline__segments\" role=\"list\">\n @for (item of renderedItems; track trackItem($index, item); let index = $index) {\n <button\n type=\"button\"\n role=\"listitem\"\n class=\"status-timeline__segment\"\n [class.status-timeline__segment--dimmed]=\"!isStatusSelected(item.status.value)\"\n [style.background-color]=\"item.status.color\"\n [style.flex-grow]=\"item.durationMs\"\n [attr.aria-label]=\"item.accessibleLabel\"\n [attr.aria-describedby]=\"config.showTooltip !== false ? tooltipId(index) : null\"\n (mouseenter)=\"activate(index)\"\n (mouseleave)=\"deactivate(index)\"\n (focus)=\"activate(index)\"\n (blur)=\"deactivate(index)\"\n (click)=\"selectItem(item)\"\n >\n <span class=\"status-timeline__sr-only\">{{ item.status.label }}</span>\n @if (config.showTooltip !== false && activeIndex === index) {\n <span\n class=\"status-timeline__tooltip\"\n [class.status-timeline__tooltip--start]=\"index === 0\"\n [class.status-timeline__tooltip--end]=\"index === renderedItems.length - 1\"\n [id]=\"tooltipId(index)\"\n role=\"tooltip\"\n >\n @for (line of item.tooltipLines; track $index) {\n <span\n class=\"status-timeline__tooltip-line\"\n [class.status-timeline__tooltip-title]=\"$index === 0\"\n >{{ line }}</span\n >\n }\n </span>\n }\n </button>\n }\n </div>\n\n @if (config.showEndpointLabels !== false) {\n <div\n class=\"status-timeline__labels\"\n [class.status-timeline__labels--start]=\"endpointLabelsAlign === 'start'\"\n [class.status-timeline__labels--center]=\"endpointLabelsAlign === 'center'\"\n [class.status-timeline__labels--end]=\"endpointLabelsAlign === 'end'\"\n aria-hidden=\"true\"\n >\n <span>{{ resolvedStartLabel }}</span>\n <span>{{ resolvedEndLabel }}</span>\n </div>\n }\n </div>\n </div>\n </div>\n\n @if (config.showLegend !== false && legendStatuses.length) {\n <div\n class=\"status-timeline__legend-row\"\n [class.status-timeline__legend-row--vertical]=\"legendLayout === 'vertical'\"\n >\n <ul\n class=\"status-timeline__legend\"\n [class.status-timeline__legend--vertical]=\"legendLayout === 'vertical'\"\n [class.status-timeline__legend--scrollable]=\"legendScrollable\"\n [class.status-timeline__legend--align-center]=\"legendAlign === 'center'\"\n [class.status-timeline__legend--align-end]=\"legendAlign === 'end'\"\n [class.status-timeline__legend--align-space-between]=\"legendAlign === 'space-between'\"\n [style.max-width]=\"legendMaxWidth\"\n [style.max-height]=\"legendMaxHeight\"\n aria-label=\"Status legend\"\n tabindex=\"0\"\n >\n @for (status of legendStatuses; track trackStatus($index, status)) {\n <li\n class=\"status-timeline__legend-item\"\n [class.status-timeline__legend-item--dimmed]=\"!isStatusSelected(status.value)\"\n [style.justify-content]=\"legendLabelJustify\"\n [style.text-align]=\"legendLabelAlign\"\n >\n <button\n type=\"button\"\n class=\"status-timeline__legend-button\"\n role=\"switch\"\n [attr.aria-checked]=\"isStatusSelected(status.value)\"\n [attr.aria-label]=\"\n (isStatusSelected(status.value) ? 'Hide ' : 'Show ') + status.label + ' intervals'\n \"\n [disabled]=\"!legendInteractive\"\n (click)=\"toggleStatus(status)\"\n >\n <span\n class=\"status-timeline__legend-swatch\"\n [style.background-color]=\"status.color\"\n aria-hidden=\"true\"\n ></span>\n <span>{{ status.label }}</span>\n </button>\n </li>\n }\n </ul>\n @if (config.legendNote) {\n <p class=\"status-timeline__legend-note\">{{ config.legendNote }}</p>\n }\n </div>\n }\n }\n</section>\n", styles: [":host{display:block;width:100%;max-width:100%;min-width:0;box-sizing:border-box;color:var(--status-timeline-text-color, #344054);font-family:var(--status-timeline-font-family, inherit)}.status-timeline{display:flex;flex-direction:column;width:100%;min-width:0}.status-timeline__visual{flex:1 1 auto;width:100%;min-width:0}.status-timeline__scroll{display:block;max-width:100%;min-width:0;width:100%;overflow-x:auto;overflow-y:visible;overscroll-behavior-inline:contain;padding:var(--status-timeline-tooltip-space, 90px) 1px 4px;box-sizing:border-box;scrollbar-width:thin}.status-timeline__scroll:focus-visible{outline:2px solid var(--status-timeline-focus-color, #2563eb);outline-offset:2px}.status-timeline__content{box-sizing:border-box}.status-timeline__segments{display:flex;align-items:stretch;gap:calc(var(--status-timeline-gap, 2) * 1px);height:var(--status-timeline-height, 36px)}.status-timeline__segment{position:relative;flex-basis:0;min-width:calc(var(--status-timeline-min-segment-width, 6) * 1px);min-height:24px;padding:0;border:0;border-radius:var(--status-timeline-segment-radius, 2px);cursor:pointer;transition:opacity .14s ease,filter .14s ease,box-shadow .14s ease}.status-timeline__segment--dimmed{opacity:var(--status-timeline-dimmed-opacity, .3);filter:saturate(.35)}.status-timeline__segment:hover,.status-timeline__segment:focus-visible{z-index:2;box-shadow:0 0 0 2px var(--status-timeline-focus-ring, rgba(37, 99, 235, .45));outline:none}.status-timeline__tooltip{position:absolute;z-index:10;display:flex;flex-direction:column;gap:2px;left:50%;bottom:calc(100% + 8px);width:max-content;min-width:var(--status-timeline-tooltip-min-width, 166px);max-width:min(300px,80vw);transform:translate(-50%);padding:8px 10px;border-radius:var(--status-timeline-tooltip-radius, 4px);background:var(--status-timeline-tooltip-background, #101828);color:var(--status-timeline-tooltip-color, #fff);box-shadow:0 4px 12px #10182833;font-size:var(--status-timeline-tooltip-font-size, 12px);line-height:1.5;text-align:left;pointer-events:none}.status-timeline__tooltip-line{display:block;white-space:normal}.status-timeline__tooltip-title{font-weight:700;white-space:nowrap}.status-timeline__tooltip--start{left:0;transform:none}.status-timeline__tooltip--end{right:0;left:auto;transform:none}.status-timeline__labels{display:flex;justify-content:space-between;gap:16px;margin-top:8px;color:var(--status-timeline-muted-color, #667085);font-size:var(--status-timeline-label-font-size, 12px)}.status-timeline__labels--start{justify-content:flex-start}.status-timeline__labels--center{justify-content:center}.status-timeline__labels--end{justify-content:flex-end}.status-timeline__legend-row{display:flex;flex-wrap:wrap;align-items:center;gap:18px;margin:12px 0 0;padding:12px 0 0;border-top:1px solid var(--status-timeline-divider-color, #eaecf0)}.status-timeline__legend{display:flex;flex:1 1 520px;min-width:0;flex-wrap:wrap;align-items:center;gap:8px 18px;margin:0;padding:0;list-style:none;scrollbar-width:thin}.status-timeline__legend--vertical{flex:0 1 auto;flex-direction:column;flex-wrap:nowrap;align-items:flex-start}.status-timeline__legend--align-center{justify-content:safe center}.status-timeline__legend--align-end{justify-content:safe flex-end}.status-timeline__legend--align-space-between{justify-content:space-between}.status-timeline__legend--scrollable:not(.status-timeline__legend--vertical){flex-wrap:nowrap;overflow-x:auto;overflow-y:hidden;overscroll-behavior-inline:contain}.status-timeline__legend--scrollable.status-timeline__legend--vertical{overflow-x:hidden;overflow-y:auto;overscroll-behavior-block:contain}.status-timeline__legend:focus-visible{outline:2px solid var(--status-timeline-focus-color, #2563eb);outline-offset:2px}.status-timeline__legend-item{display:inline-flex;align-items:center;font-size:var(--status-timeline-legend-font-size, 12px);white-space:nowrap;transition:opacity .14s ease}.status-timeline__legend-item--dimmed{opacity:var(--status-timeline-dimmed-opacity, .3)}.status-timeline__legend-button{display:inline-flex;align-items:center;justify-content:inherit;width:100%;gap:6px;margin:0;padding:2px 0;border:0;background:transparent;color:inherit;font:inherit;text-align:inherit;white-space:inherit;cursor:pointer}.status-timeline__legend-button:disabled{opacity:1;cursor:default}.status-timeline__legend-button:focus-visible{border-radius:2px;outline:2px solid var(--status-timeline-focus-color, #2563eb);outline-offset:2px}.status-timeline__legend--vertical .status-timeline__legend-item{width:100%;box-sizing:border-box}.status-timeline__legend-swatch{width:10px;height:10px;flex:0 0 10px;border-radius:var(--status-timeline-legend-radius, 2px)}.status-timeline__legend-note{flex:1 1 300px;margin:0;color:var(--status-timeline-muted-color, #667085);font-size:var(--status-timeline-legend-font-size, 12px);text-align:right}.status-timeline--legend-top .status-timeline__legend-row{order:-1;margin:0 0 12px;padding:0 0 12px;border-top:0;border-bottom:1px solid var(--status-timeline-divider-color, #eaecf0)}.status-timeline--legend-left,.status-timeline--legend-right{flex-direction:row;align-items:stretch;gap:12px}.status-timeline--legend-left .status-timeline__legend-row,.status-timeline--legend-right .status-timeline__legend-row{flex:0 1 auto;flex-direction:column;align-items:flex-start;width:max-content;max-width:min(280px,40%);margin:0}.status-timeline--legend-left .status-timeline__legend-row{order:-1;padding:0 12px 0 0;border-top:0;border-right:1px solid var(--status-timeline-divider-color, #eaecf0)}.status-timeline--legend-right .status-timeline__legend-row{padding:0 0 0 12px;border-top:0;border-left:1px solid var(--status-timeline-divider-color, #eaecf0)}.status-timeline__legend-row--vertical .status-timeline__legend-note{text-align:left}.status-timeline__state{min-height:var(--status-timeline-state-height, 72px);display:flex;align-items:center;justify-content:center;gap:10px;padding:12px;box-sizing:border-box;color:var(--status-timeline-muted-color, #667085);border:1px dashed var(--status-timeline-divider-color, #d0d5dd);border-radius:4px;font-size:13px}.status-timeline__state--error{color:var(--status-timeline-error-color, #b42318);background:var(--status-timeline-error-background, #fef3f2)}.status-timeline__skeleton{width:min(180px,45%);height:12px;border-radius:3px;background:linear-gradient(90deg,#eaecf0 25%,#f9fafb,#eaecf0 75%);background-size:200% 100%;animation:status-timeline-shimmer 1.5s infinite}.status-timeline__sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@keyframes status-timeline-shimmer{to{background-position-x:-200%}}@media (prefers-reduced-motion: reduce){.status-timeline__skeleton{animation:none}.status-timeline__segment,.status-timeline__legend-item{transition:none}}@media (max-width: 720px){.status-timeline--legend-left,.status-timeline--legend-right{flex-direction:column}.status-timeline--legend-left .status-timeline__legend-row,.status-timeline--legend-right .status-timeline__legend-row{width:100%;max-width:100%}.status-timeline--legend-left .status-timeline__legend-row{margin:0 0 12px;padding:0 0 12px;border-right:0;border-bottom:1px solid var(--status-timeline-divider-color, #eaecf0)}.status-timeline--legend-right .status-timeline__legend-row{margin:12px 0 0;padding:12px 0 0;border-top:1px solid var(--status-timeline-divider-color, #eaecf0);border-left:0}.status-timeline__legend-row{align-items:flex-start;flex-direction:column}.status-timeline__legend-note{text-align:left}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4301
+ }
4302
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: StatusTimelineComponent, decorators: [{
4303
+ type: Component,
4304
+ args: [{ selector: 'lib-status-timeline', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<section\n class=\"status-timeline\"\n [style.--status-timeline-gap]=\"config.segmentGap ?? 2\"\n [style.--status-timeline-min-segment-width]=\"config.minSegmentWidth ?? 6\"\n [style.--status-timeline-dimmed-opacity]=\"config.legendDimmedOpacity ?? 0.3\"\n [class.status-timeline--legend-top]=\"legendPosition === 'top'\"\n [class.status-timeline--legend-bottom]=\"legendPosition === 'bottom'\"\n [class.status-timeline--legend-left]=\"legendPosition === 'left'\"\n [class.status-timeline--legend-right]=\"legendPosition === 'right'\"\n [attr.aria-label]=\"config.ariaLabel || 'Status timeline'\"\n>\n @if (config.loading) {\n <div class=\"status-timeline__state\" role=\"status\" aria-live=\"polite\">\n <span class=\"status-timeline__skeleton\" aria-hidden=\"true\"></span>\n <span>{{ config.loadingLabel || 'Loading timeline' }}</span>\n </div>\n } @else if (config.error) {\n <div class=\"status-timeline__state status-timeline__state--error\" role=\"alert\">\n {{ config.error }}\n </div>\n } @else if (!renderedItems.length) {\n <div class=\"status-timeline__state\" role=\"status\">\n {{ config.emptyLabel || 'No timeline data available' }}\n </div>\n } @else {\n <div class=\"status-timeline__visual\">\n <div class=\"status-timeline__scroll\" tabindex=\"0\" aria-label=\"Scrollable status intervals\">\n <div class=\"status-timeline__content\" [style.min-width]=\"timelineMinWidth\">\n <div class=\"status-timeline__segments\" role=\"list\">\n @for (item of renderedItems; track trackItem($index, item); let index = $index) {\n <button\n type=\"button\"\n role=\"listitem\"\n class=\"status-timeline__segment\"\n [class.status-timeline__segment--dimmed]=\"!isStatusSelected(item.status.value)\"\n [style.background-color]=\"item.status.color\"\n [style.flex-grow]=\"item.durationMs\"\n [attr.aria-label]=\"item.accessibleLabel\"\n [attr.aria-describedby]=\"config.showTooltip !== false ? tooltipId(index) : null\"\n (mouseenter)=\"activate(index)\"\n (mouseleave)=\"deactivate(index)\"\n (focus)=\"activate(index)\"\n (blur)=\"deactivate(index)\"\n (click)=\"selectItem(item)\"\n >\n <span class=\"status-timeline__sr-only\">{{ item.status.label }}</span>\n @if (config.showTooltip !== false && activeIndex === index) {\n <span\n class=\"status-timeline__tooltip\"\n [class.status-timeline__tooltip--start]=\"index === 0\"\n [class.status-timeline__tooltip--end]=\"index === renderedItems.length - 1\"\n [id]=\"tooltipId(index)\"\n role=\"tooltip\"\n >\n @for (line of item.tooltipLines; track $index) {\n <span\n class=\"status-timeline__tooltip-line\"\n [class.status-timeline__tooltip-title]=\"$index === 0\"\n >{{ line }}</span\n >\n }\n </span>\n }\n </button>\n }\n </div>\n\n @if (config.showEndpointLabels !== false) {\n <div\n class=\"status-timeline__labels\"\n [class.status-timeline__labels--start]=\"endpointLabelsAlign === 'start'\"\n [class.status-timeline__labels--center]=\"endpointLabelsAlign === 'center'\"\n [class.status-timeline__labels--end]=\"endpointLabelsAlign === 'end'\"\n aria-hidden=\"true\"\n >\n <span>{{ resolvedStartLabel }}</span>\n <span>{{ resolvedEndLabel }}</span>\n </div>\n }\n </div>\n </div>\n </div>\n\n @if (config.showLegend !== false && legendStatuses.length) {\n <div\n class=\"status-timeline__legend-row\"\n [class.status-timeline__legend-row--vertical]=\"legendLayout === 'vertical'\"\n >\n <ul\n class=\"status-timeline__legend\"\n [class.status-timeline__legend--vertical]=\"legendLayout === 'vertical'\"\n [class.status-timeline__legend--scrollable]=\"legendScrollable\"\n [class.status-timeline__legend--align-center]=\"legendAlign === 'center'\"\n [class.status-timeline__legend--align-end]=\"legendAlign === 'end'\"\n [class.status-timeline__legend--align-space-between]=\"legendAlign === 'space-between'\"\n [style.max-width]=\"legendMaxWidth\"\n [style.max-height]=\"legendMaxHeight\"\n aria-label=\"Status legend\"\n tabindex=\"0\"\n >\n @for (status of legendStatuses; track trackStatus($index, status)) {\n <li\n class=\"status-timeline__legend-item\"\n [class.status-timeline__legend-item--dimmed]=\"!isStatusSelected(status.value)\"\n [style.justify-content]=\"legendLabelJustify\"\n [style.text-align]=\"legendLabelAlign\"\n >\n <button\n type=\"button\"\n class=\"status-timeline__legend-button\"\n role=\"switch\"\n [attr.aria-checked]=\"isStatusSelected(status.value)\"\n [attr.aria-label]=\"\n (isStatusSelected(status.value) ? 'Hide ' : 'Show ') + status.label + ' intervals'\n \"\n [disabled]=\"!legendInteractive\"\n (click)=\"toggleStatus(status)\"\n >\n <span\n class=\"status-timeline__legend-swatch\"\n [style.background-color]=\"status.color\"\n aria-hidden=\"true\"\n ></span>\n <span>{{ status.label }}</span>\n </button>\n </li>\n }\n </ul>\n @if (config.legendNote) {\n <p class=\"status-timeline__legend-note\">{{ config.legendNote }}</p>\n }\n </div>\n }\n }\n</section>\n", styles: [":host{display:block;width:100%;max-width:100%;min-width:0;box-sizing:border-box;color:var(--status-timeline-text-color, #344054);font-family:var(--status-timeline-font-family, inherit)}.status-timeline{display:flex;flex-direction:column;width:100%;min-width:0}.status-timeline__visual{flex:1 1 auto;width:100%;min-width:0}.status-timeline__scroll{display:block;max-width:100%;min-width:0;width:100%;overflow-x:auto;overflow-y:visible;overscroll-behavior-inline:contain;padding:var(--status-timeline-tooltip-space, 90px) 1px 4px;box-sizing:border-box;scrollbar-width:thin}.status-timeline__scroll:focus-visible{outline:2px solid var(--status-timeline-focus-color, #2563eb);outline-offset:2px}.status-timeline__content{box-sizing:border-box}.status-timeline__segments{display:flex;align-items:stretch;gap:calc(var(--status-timeline-gap, 2) * 1px);height:var(--status-timeline-height, 36px)}.status-timeline__segment{position:relative;flex-basis:0;min-width:calc(var(--status-timeline-min-segment-width, 6) * 1px);min-height:24px;padding:0;border:0;border-radius:var(--status-timeline-segment-radius, 2px);cursor:pointer;transition:opacity .14s ease,filter .14s ease,box-shadow .14s ease}.status-timeline__segment--dimmed{opacity:var(--status-timeline-dimmed-opacity, .3);filter:saturate(.35)}.status-timeline__segment:hover,.status-timeline__segment:focus-visible{z-index:2;box-shadow:0 0 0 2px var(--status-timeline-focus-ring, rgba(37, 99, 235, .45));outline:none}.status-timeline__tooltip{position:absolute;z-index:10;display:flex;flex-direction:column;gap:2px;left:50%;bottom:calc(100% + 8px);width:max-content;min-width:var(--status-timeline-tooltip-min-width, 166px);max-width:min(300px,80vw);transform:translate(-50%);padding:8px 10px;border-radius:var(--status-timeline-tooltip-radius, 4px);background:var(--status-timeline-tooltip-background, #101828);color:var(--status-timeline-tooltip-color, #fff);box-shadow:0 4px 12px #10182833;font-size:var(--status-timeline-tooltip-font-size, 12px);line-height:1.5;text-align:left;pointer-events:none}.status-timeline__tooltip-line{display:block;white-space:normal}.status-timeline__tooltip-title{font-weight:700;white-space:nowrap}.status-timeline__tooltip--start{left:0;transform:none}.status-timeline__tooltip--end{right:0;left:auto;transform:none}.status-timeline__labels{display:flex;justify-content:space-between;gap:16px;margin-top:8px;color:var(--status-timeline-muted-color, #667085);font-size:var(--status-timeline-label-font-size, 12px)}.status-timeline__labels--start{justify-content:flex-start}.status-timeline__labels--center{justify-content:center}.status-timeline__labels--end{justify-content:flex-end}.status-timeline__legend-row{display:flex;flex-wrap:wrap;align-items:center;gap:18px;margin:12px 0 0;padding:12px 0 0;border-top:1px solid var(--status-timeline-divider-color, #eaecf0)}.status-timeline__legend{display:flex;flex:1 1 520px;min-width:0;flex-wrap:wrap;align-items:center;gap:8px 18px;margin:0;padding:0;list-style:none;scrollbar-width:thin}.status-timeline__legend--vertical{flex:0 1 auto;flex-direction:column;flex-wrap:nowrap;align-items:flex-start}.status-timeline__legend--align-center{justify-content:safe center}.status-timeline__legend--align-end{justify-content:safe flex-end}.status-timeline__legend--align-space-between{justify-content:space-between}.status-timeline__legend--scrollable:not(.status-timeline__legend--vertical){flex-wrap:nowrap;overflow-x:auto;overflow-y:hidden;overscroll-behavior-inline:contain}.status-timeline__legend--scrollable.status-timeline__legend--vertical{overflow-x:hidden;overflow-y:auto;overscroll-behavior-block:contain}.status-timeline__legend:focus-visible{outline:2px solid var(--status-timeline-focus-color, #2563eb);outline-offset:2px}.status-timeline__legend-item{display:inline-flex;align-items:center;font-size:var(--status-timeline-legend-font-size, 12px);white-space:nowrap;transition:opacity .14s ease}.status-timeline__legend-item--dimmed{opacity:var(--status-timeline-dimmed-opacity, .3)}.status-timeline__legend-button{display:inline-flex;align-items:center;justify-content:inherit;width:100%;gap:6px;margin:0;padding:2px 0;border:0;background:transparent;color:inherit;font:inherit;text-align:inherit;white-space:inherit;cursor:pointer}.status-timeline__legend-button:disabled{opacity:1;cursor:default}.status-timeline__legend-button:focus-visible{border-radius:2px;outline:2px solid var(--status-timeline-focus-color, #2563eb);outline-offset:2px}.status-timeline__legend--vertical .status-timeline__legend-item{width:100%;box-sizing:border-box}.status-timeline__legend-swatch{width:10px;height:10px;flex:0 0 10px;border-radius:var(--status-timeline-legend-radius, 2px)}.status-timeline__legend-note{flex:1 1 300px;margin:0;color:var(--status-timeline-muted-color, #667085);font-size:var(--status-timeline-legend-font-size, 12px);text-align:right}.status-timeline--legend-top .status-timeline__legend-row{order:-1;margin:0 0 12px;padding:0 0 12px;border-top:0;border-bottom:1px solid var(--status-timeline-divider-color, #eaecf0)}.status-timeline--legend-left,.status-timeline--legend-right{flex-direction:row;align-items:stretch;gap:12px}.status-timeline--legend-left .status-timeline__legend-row,.status-timeline--legend-right .status-timeline__legend-row{flex:0 1 auto;flex-direction:column;align-items:flex-start;width:max-content;max-width:min(280px,40%);margin:0}.status-timeline--legend-left .status-timeline__legend-row{order:-1;padding:0 12px 0 0;border-top:0;border-right:1px solid var(--status-timeline-divider-color, #eaecf0)}.status-timeline--legend-right .status-timeline__legend-row{padding:0 0 0 12px;border-top:0;border-left:1px solid var(--status-timeline-divider-color, #eaecf0)}.status-timeline__legend-row--vertical .status-timeline__legend-note{text-align:left}.status-timeline__state{min-height:var(--status-timeline-state-height, 72px);display:flex;align-items:center;justify-content:center;gap:10px;padding:12px;box-sizing:border-box;color:var(--status-timeline-muted-color, #667085);border:1px dashed var(--status-timeline-divider-color, #d0d5dd);border-radius:4px;font-size:13px}.status-timeline__state--error{color:var(--status-timeline-error-color, #b42318);background:var(--status-timeline-error-background, #fef3f2)}.status-timeline__skeleton{width:min(180px,45%);height:12px;border-radius:3px;background:linear-gradient(90deg,#eaecf0 25%,#f9fafb,#eaecf0 75%);background-size:200% 100%;animation:status-timeline-shimmer 1.5s infinite}.status-timeline__sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@keyframes status-timeline-shimmer{to{background-position-x:-200%}}@media (prefers-reduced-motion: reduce){.status-timeline__skeleton{animation:none}.status-timeline__segment,.status-timeline__legend-item{transition:none}}@media (max-width: 720px){.status-timeline--legend-left,.status-timeline--legend-right{flex-direction:column}.status-timeline--legend-left .status-timeline__legend-row,.status-timeline--legend-right .status-timeline__legend-row{width:100%;max-width:100%}.status-timeline--legend-left .status-timeline__legend-row{margin:0 0 12px;padding:0 0 12px;border-right:0;border-bottom:1px solid var(--status-timeline-divider-color, #eaecf0)}.status-timeline--legend-right .status-timeline__legend-row{margin:12px 0 0;padding:12px 0 0;border-top:1px solid var(--status-timeline-divider-color, #eaecf0);border-left:0}.status-timeline__legend-row{align-items:flex-start;flex-direction:column}.status-timeline__legend-note{text-align:left}}\n"] }]
4305
+ }], propDecorators: { config: [{
4306
+ type: Input,
4307
+ args: [{ required: true }]
4308
+ }], chartEvent: [{
4309
+ type: Output
4310
+ }] } });
4311
+
4108
4312
  /**
4109
4313
  * Generated bundle index. Do not edit.
4110
4314
  */
4111
4315
 
4112
- export { BarChart, ChartLegendAdapter, ChartLegendComponent, Charts, CustomChartComponent, DoughnutChart, Heatmap, LinesChart, PieChart, RightClickCard, SankeyChart, ScatterChartComp, StackedAreaChart };
4316
+ export { BarChart, ChartLegendAdapter, ChartLegendComponent, Charts, CustomChartComponent, DoughnutChart, Heatmap, LinesChart, PieChart, RightClickCard, SankeyChart, ScatterChartComp, StackedAreaChart, StatusTimelineComponent };
4113
4317
  //# sourceMappingURL=cats-charts.mjs.map