From 73219a8d6ea3cba6ee82d9a032f2fb984d11d8d8 Mon Sep 17 00:00:00 2001 From: Kacper Olszanski Date: Thu, 22 Apr 2021 13:58:36 +0200 Subject: [PATCH] feat(event-chart): APM-295388 Implemented "heatfield" into event-chart --- apps/demos/src/app-routing.module.ts | 5 + apps/demos/src/nav-items.ts | 4 + .../dev/src/event-chart/big-time-test-data.ts | 5 + .../src/event-chart/easy-travel-test-data.ts | 37 ++ .../src/event-chart/event-chart-demo-data.ts | 9 + .../event-chart-demo.component.html | 32 ++ .../event-chart/event-chart-demo.component.ts | 3 + .../event-chart/mobile-actions-test-data.ts | 5 + .../event-chart/session-replay-test-data.ts | 5 + libs/barista-components/event-chart/README.md | 41 ++ .../event-chart/src/event-chart-directives.ts | 94 +++++ .../event-chart/src/event-chart-module.ts | 4 + .../event-chart/src/event-chart.html | 275 +++++++++----- .../event-chart/src/event-chart.scss | 30 +- .../event-chart/src/event-chart.spec.ts | 63 +++- .../event-chart/src/event-chart.ts | 354 ++++++++++++++++-- .../src/merge-and-path/merge-events.ts | 83 +++- .../event-chart/src/render-event.interface.ts | 22 ++ libs/examples/src/event-chart/BUILD.bazel | 1 + .../event-chart-examples.module.ts | 2 + .../event-chart-heatfield-example.html | 51 +++ .../event-chart-heatfield-example.ts | 115 ++++++ libs/examples/src/event-chart/index.ts | 1 + libs/examples/src/index.ts | 3 + 24 files changed, 1109 insertions(+), 135 deletions(-) create mode 100644 libs/examples/src/event-chart/event-chart-heatfield-example/event-chart-heatfield-example.html create mode 100644 libs/examples/src/event-chart/event-chart-heatfield-example/event-chart-heatfield-example.ts diff --git a/apps/demos/src/app-routing.module.ts b/apps/demos/src/app-routing.module.ts index bb62bbfec2..ee85b34181 100644 --- a/apps/demos/src/app-routing.module.ts +++ b/apps/demos/src/app-routing.module.ts @@ -123,6 +123,7 @@ import { DtExampleEventChartOverlappingLoad, DtExampleEventChartOverlay, DtExampleEventChartSelection, + DtExampleEventChartHeatfield, DtExampleEventChartSessionReplay, DtExampleExpandablePanelDefault, DtExampleExpandablePanelDisabled, @@ -643,6 +644,10 @@ const ROUTES: Routes = [ path: 'event-chart-overlay-example', component: DtExampleEventChartOverlay, }, + { + path: 'event-chart-heatfield-example', + component: DtExampleEventChartHeatfield, + }, { path: 'event-chart-selection-example', component: DtExampleEventChartSelection, diff --git a/apps/demos/src/nav-items.ts b/apps/demos/src/nav-items.ts index 41cac62cae..4263ca52b9 100644 --- a/apps/demos/src/nav-items.ts +++ b/apps/demos/src/nav-items.ts @@ -549,6 +549,10 @@ export const DT_DEMOS_EXAMPLE_NAV_ITEMS = [ name: 'event-chart-overlay-example', route: '/event-chart-overlay-example', }, + { + name: 'event-chart-heatfield-example', + route: '/event-chart-heatfield-example', + }, { name: 'event-chart-selection-example', route: '/event-chart-selection-example', diff --git a/apps/dev/src/event-chart/big-time-test-data.ts b/apps/dev/src/event-chart/big-time-test-data.ts index 3118224b2d..826cf9e57f 100644 --- a/apps/dev/src/event-chart/big-time-test-data.ts +++ b/apps/dev/src/event-chart/big-time-test-data.ts @@ -21,6 +21,7 @@ import { EventChartDemoEvent, EventChartDemoLane, EventChartDemoLegendItem, + EventChartDemoHeatfield, } from './event-chart-demo-data'; // tslint:disable: max-file-line-count @@ -218,4 +219,8 @@ export class BigTimeDataSource implements EventChartDemoDataSource { } return Array.from(lanesPerColor.values()); } + + getHeatfields(): EventChartDemoHeatfield[] { + return []; + } } diff --git a/apps/dev/src/event-chart/easy-travel-test-data.ts b/apps/dev/src/event-chart/easy-travel-test-data.ts index 791c5743d8..3151715a37 100644 --- a/apps/dev/src/event-chart/easy-travel-test-data.ts +++ b/apps/dev/src/event-chart/easy-travel-test-data.ts @@ -19,6 +19,7 @@ import { EventChartDemoEvent, EventChartDemoLane, EventChartDemoLegendItem, + EventChartDemoHeatfield, } from './event-chart-demo-data'; export const EASY_TRAVEL_TEST_DATA = [ @@ -179,4 +180,40 @@ export class EasyTravelDataSource implements EventChartDemoDataSource { } return Array.from(lanesPerColor.values()); } + + getHeatfields(): EventChartDemoHeatfield[] { + return [ + { + end: 1250, + data: { + page: '/home', + pageGroup: '/home', + }, + }, + { + start: 1250, + end: 2000, + data: { + page: '/booking/asdf', + pageGroup: '/booking', + }, + color: 'error', + }, + { + start: 2000, + end: 3000, + data: { + page: '/cart/asdf2', + pageGroup: '/cart', + }, + }, + { + start: 3000, + data: { + page: '/finish', + pageGroup: '/finish', + }, + }, + ]; + } } diff --git a/apps/dev/src/event-chart/event-chart-demo-data.ts b/apps/dev/src/event-chart/event-chart-demo-data.ts index b77fb16af9..455ab7cb90 100644 --- a/apps/dev/src/event-chart/event-chart-demo-data.ts +++ b/apps/dev/src/event-chart/event-chart-demo-data.ts @@ -23,6 +23,14 @@ export interface EventChartDemoEvent { data?: any; } +export interface EventChartDemoHeatfield { + start?: number; + end?: number; + color?: 'default' | 'error' | 'filtered'; + // tslint:disable-next-line: no-any + data?: any; +} + export interface EventChartDemoLane { name: string; label: string; @@ -38,4 +46,5 @@ export interface EventChartDemoDataSource { getEvents(): EventChartDemoEvent[]; getLanes(): EventChartDemoLane[]; getLegendItems(): EventChartDemoLegendItem[]; + getHeatfields(): EventChartDemoHeatfield[]; } diff --git a/apps/dev/src/event-chart/event-chart-demo.component.html b/apps/dev/src/event-chart/event-chart-demo.component.html index 2e14216cbc..451ff22b8f 100644 --- a/apps/dev/src/event-chart/event-chart-demo.component.html +++ b/apps/dev/src/event-chart/event-chart-demo.component.html @@ -1,4 +1,18 @@ +

Event Chart

+
+
+
+ + + + + + +
+

Page {{ t.data.page }}

+ + + Page + {{ t.data.page }} + + + Page group + {{ + t.data.pageGroup + }} + + +
+
diff --git a/apps/dev/src/event-chart/event-chart-demo.component.ts b/apps/dev/src/event-chart/event-chart-demo.component.ts index 28517b2e74..9b58efe008 100644 --- a/apps/dev/src/event-chart/event-chart-demo.component.ts +++ b/apps/dev/src/event-chart/event-chart-demo.component.ts @@ -28,6 +28,7 @@ import { EventChartDemoEvent, EventChartDemoLane, EventChartDemoLegendItem, + EventChartDemoHeatfield, } from './event-chart-demo-data'; import { MobileActionDataSource } from './mobile-actions-test-data'; import { SessionReplayDataSource } from './session-replay-test-data'; @@ -56,6 +57,7 @@ export class EventChartDemo { _events: EventChartDemoEvent[] = []; _lanes: EventChartDemoLane[] = []; _legendItems: EventChartDemoLegendItem[] = []; + _heatfields: EventChartDemoHeatfield[] = []; get _selectedDataSet(): DataSet { return this._ds; @@ -65,6 +67,7 @@ export class EventChartDemo { this._events = value.dataSource.getEvents(); this._lanes = value.dataSource.getLanes(); this._legendItems = value.dataSource.getLegendItems(); + this._heatfields = value.dataSource.getHeatfields(); this._changeDetectorRef.markForCheck(); } private _ds = DATA_SETS[1]; diff --git a/apps/dev/src/event-chart/mobile-actions-test-data.ts b/apps/dev/src/event-chart/mobile-actions-test-data.ts index 3aa244c158..27859b5e8d 100644 --- a/apps/dev/src/event-chart/mobile-actions-test-data.ts +++ b/apps/dev/src/event-chart/mobile-actions-test-data.ts @@ -21,6 +21,7 @@ import { EventChartDemoEvent, EventChartDemoLane, EventChartDemoLegendItem, + EventChartDemoHeatfield, } from './event-chart-demo-data'; // tslint:disable: max-file-line-count @@ -455,6 +456,10 @@ const TEST_DATA = [ ]; export class MobileActionDataSource implements EventChartDemoDataSource { + getHeatfields(): EventChartDemoHeatfield[] { + return []; + } + getEvents(): EventChartDemoEvent[] { const events: EventChartDemoEvent[] = []; TEST_DATA.forEach((event) => { diff --git a/apps/dev/src/event-chart/session-replay-test-data.ts b/apps/dev/src/event-chart/session-replay-test-data.ts index 78adcaf295..e256460af0 100644 --- a/apps/dev/src/event-chart/session-replay-test-data.ts +++ b/apps/dev/src/event-chart/session-replay-test-data.ts @@ -19,6 +19,7 @@ import { EventChartDemoEvent, EventChartDemoLane, EventChartDemoLegendItem, + EventChartDemoHeatfield, } from './event-chart-demo-data'; // tslint:disable: max-file-line-count @@ -3286,4 +3287,8 @@ export class SessionReplayDataSource implements EventChartDemoDataSource { } return Array.from(lanesPerColor.values()); } + + getHeatfields(): EventChartDemoHeatfield[] { + return []; + } } diff --git a/libs/barista-components/event-chart/README.md b/libs/barista-components/event-chart/README.md index 90e25110d4..4fedb9cf60 100644 --- a/libs/barista-components/event-chart/README.md +++ b/libs/barista-components/event-chart/README.md @@ -65,6 +65,27 @@ metadata for this event. | ---------- | ------------------------------------------- | -------------------------------------------------- | | `selected` | `EventEmitter` | Event that fires when a an eventBubble is clicked. | +### DtEventChartHeatfield + +The `DtEventChartHeatfield` component is used to provide heatfield data to the +parent event chart. It will also accept an arbitrary `data` input, which can +hold metadata for this field. + +#### Inputs + +| Name | Type | Default | Description | +| ------- | -------------------- | --------- | --------------------------------------------------------------------------------- | +| `start` | `number` | | The start numerical/date value on the x-axis of the chart. | +| `end` | `number` | | LThe end numerical/date value on the x-axis of the chart. | +| `color` | `DtEventChartColors` | `default` | Color of the heatfield. | +| `data` | `T` | - | Any data for this heatfield. This data will be emitted when an event is selected. | + +#### Outputs + +| Name | Type | Description | +| ---------- | ------------------------------------------- | ------------------------------------------------ | +| `selected` | `EventEmitter` | Event that fires when a an heatfield is clicked. | + ### DtEventChartLane The `dtEventChartLane` defines a lane, on which an event will be rendered. The @@ -112,6 +133,22 @@ context to the overlay template, which can be used as following: ``` +### DtEventChartHeatfieldOverlay + +The `dtEventChartHeatfieldOverlay` directive applies to an `ng-template` element +lets you provide a template for the rendered overlay. The overlay will be shown +when a user hovers the heatfield. The EventChart will expose the hovered events +(this is always an array, as the events could be clustered) as `$implicit` +context to the overlay template, which can be used as following: + +```html + +
+ +
+
+``` + ## DtEventChartColors Currently, there are only four different colors which are applicable to a @@ -140,6 +177,10 @@ Currently, there are only four different colors which are applicable to a +### Setting heatfields and heatfield overlay template + + + ### Handling event selection via click diff --git a/libs/barista-components/event-chart/src/event-chart-directives.ts b/libs/barista-components/event-chart/src/event-chart-directives.ts index 72f6737fd0..8ebf4981a9 100644 --- a/libs/barista-components/event-chart/src/event-chart-directives.ts +++ b/libs/barista-components/event-chart/src/event-chart-directives.ts @@ -53,6 +53,15 @@ export class DtEventChartSelectedEvent { constructor(public sources: DtEventChartEvent[]) {} } +/** + * Selected field class. + * An instance of this class will be emitted, when a field is clicked + * by the user. + */ +export class DtEventChartSelectedField { + constructor(public sources: DtEventChartField[]) {} +} + @Directive({ selector: 'ng-template[dtEventChartOverlay], ng-template[dtSausageChartOverlay]', @@ -60,6 +69,13 @@ export class DtEventChartSelectedEvent { }) export class DtEventChartOverlay {} +@Directive({ + selector: + 'ng-template[dtEventChartHeatfieldOverlay], ng-template[dtSausageChartHeatfieldOverlay]', + exportAs: 'dtEventChartHeatfieldOverlay', +}) +export class DtEventChartHeatfieldOverlay {} + @Component({ selector: 'dt-event-chart-event, dt-sausage-chart-event', exportAs: 'dtEventChartEvent', @@ -235,3 +251,81 @@ export class DtEventChartLegendItem implements OnChanges, OnDestroy { this._stateChanges$.complete(); } } + +@Component({ + selector: 'dt-event-chart-field, dt-sausage-chart-field', + exportAs: 'dtEventChartField', + template: '', +}) +export class DtEventChartField implements OnChanges, OnDestroy { + /** Start on the xAxis of the chart for the heatfield */ + @Input() + get start(): number | null { + return this._start; + } + + set start(value: number | null) { + if (value === this._start) { + return; + } + this._start = value == undefined ? null : coerceNumberProperty(value); + } + + private _start: number | null = null; + static ngAcceptInputType_start: NumberInput; + + /** End on the xAxis of the chart for the heatfield */ + @Input() + get end(): number | null { + return this._end; + } + + set end(value: number | null) { + if (value === this._end) { + return; + } + this._end = value == undefined ? null : coerceNumberProperty(value); + } + + private _end: number | null = null; + static ngAcceptInputType_end: NumberInput; + + /** Defines the color for this field. */ + @Input() color: DtEventChartColors = 'default'; + + /** + * Data of the field. This can be freely given and the data will be exposed + * to the consumer, when the field is clicked or passed through to the + * implicit overlay context. + */ + @Input() data: T; + + /** Emits when field is selected. */ + @Output() readonly selected = new EventEmitter< + DtEventChartSelectedField + >(); + + /** + * @internal + * Content template, which will be used to render the ng-content of + * this component into the legend item + */ + @ViewChild(TemplateRef, { static: true }) + _contentTemplate: TemplateRef; + + /** + * @internal + * Subject that fires when either of the inputs changes, the + * dtEventChart will subscribe to a collection of these stateChanges + * events to react to changes in the data of DtEventChartLegendItem. + */ + _stateChanges$ = new Subject(); + + ngOnChanges(): void { + this._stateChanges$.next(); + } + + ngOnDestroy(): void { + this._stateChanges$.complete(); + } +} diff --git a/libs/barista-components/event-chart/src/event-chart-module.ts b/libs/barista-components/event-chart/src/event-chart-module.ts index 666834eeb9..af367ac1e8 100644 --- a/libs/barista-components/event-chart/src/event-chart-module.ts +++ b/libs/barista-components/event-chart/src/event-chart-module.ts @@ -26,6 +26,8 @@ import { DtEventChartLane, DtEventChartLegendItem, DtEventChartOverlay, + DtEventChartHeatfieldOverlay, + DtEventChartField, } from './event-chart-directives'; import { DtEventChartLegend } from './event-chart-legend'; import { DtFormattersModule } from '@dynatrace/barista-components/formatters'; @@ -36,6 +38,8 @@ export const DT_EVENT_CHART_DIRECTIVES = [ DtEventChartLane, DtEventChartLegendItem, DtEventChartOverlay, + DtEventChartHeatfieldOverlay, + DtEventChartField, ]; @NgModule({ diff --git a/libs/barista-components/event-chart/src/event-chart.html b/libs/barista-components/event-chart/src/event-chart.html index d9d4ba25c1..50377ab42c 100644 --- a/libs/barista-components/event-chart/src/event-chart.html +++ b/libs/barista-components/event-chart/src/event-chart.html @@ -1,7 +1,11 @@
-
+
{{ lane.label }}
@@ -13,114 +17,189 @@ class="dt-event-chart-canvas-svg" [attr.height]="_svgHeight" > - - - - - + + + + + + + + + - - + + - + + - - {{ tick.value }} - + - - + + {{ tick.value }} + - - + + - + + + + + + - - - - - {{ renderEvent.events.length }} - - + + + + {{ renderEvent.events.length }} + + +
diff --git a/libs/barista-components/event-chart/src/event-chart.scss b/libs/barista-components/event-chart/src/event-chart.scss index 998bfd38ee..13bba01dba 100644 --- a/libs/barista-components/event-chart/src/event-chart.scss +++ b/libs/barista-components/event-chart/src/event-chart.scss @@ -70,25 +70,45 @@ } } -.dt-event-chart-event-selectedoutline { +// Single chart field +.dt-event-chart-field { + stroke-width: 4; + paint-order: stroke fill; + stroke: #ffffff; + fill: $dt-event-chart-default-color; + cursor: pointer; + + &:focus { + @include dt-interactive-reset(); + } +} + +.dt-event-chart-rect { + paint-order: fill; + fill: $dt-event-chart-default-color; + opacity: 0.125; + z-index: 200; +} + +.dt-event-chart-style-selectedoutline { stroke: $dt-event-chart-select-color; stroke-width: 2; fill: #ffffff; } -.dt-event-chart-event-error { +.dt-event-chart-style-error { fill: $dt-event-chart-error-color; } -.dt-event-chart-event-filtered { +.dt-event-chart-style-filtered { fill: $dt-event-chart-filtered-color; } -.dt-event-chart-event-conversion { +.dt-event-chart-style-conversion { fill: $dt-event-chart-conversion-color; } -.dt-event-chart-event-mergednumber { +.dt-event-chart-style-mergednumber { fill: #ffffff; pointer-events: none; font-size: 10; diff --git a/libs/barista-components/event-chart/src/event-chart.spec.ts b/libs/barista-components/event-chart/src/event-chart.spec.ts index 83615063cb..e6c03f4839 100644 --- a/libs/barista-components/event-chart/src/event-chart.spec.ts +++ b/libs/barista-components/event-chart/src/event-chart.spec.ts @@ -35,7 +35,7 @@ import { /** Gets the rendered merged numbering. */ function getRenderedMergedTextLabels(fixture: ComponentFixture): string[] { const texts = fixture.debugElement.queryAll( - By.css('.dt-event-chart-event-mergednumber'), + By.css('.dt-event-chart-style-mergednumber'), ); return texts.map((text) => text.nativeElement.innerHTML.trim()); } @@ -150,6 +150,12 @@ describe('DtEventChart', () => { expect(fixture).toBeDefined(); }); + it('should render the heatfields correctly', () => { + const renderedFields = + fixture.componentInstance._eventChartInstance._renderFields; + expect(renderedFields).toHaveLength(3); + }); + it('should render the events correctly', () => { const renderedEvents = fixture.componentInstance._eventChartInstance._renderEvents; @@ -648,7 +654,7 @@ describe('DtEventChart', () => { it('should not have anything selected initially', () => { const selectedEvent = fixture.debugElement.query( - By.css('.dt-event-chart-event-selected'), + By.css('.dt-event-chart-style-selected'), ); expect(selectedEvent).toBeNull(); }); @@ -670,7 +676,7 @@ describe('DtEventChart', () => { fixture.componentInstance._eventChartInstance.deselect(); fixture.detectChanges(); const selectedEvent = fixture.debugElement.query( - By.css('.dt-event-chart-event-selected'), + By.css('.dt-event-chart-style-selected'), ); expect(selectedEvent).toBeNull(); }); @@ -726,6 +732,23 @@ describe('DtEventChart', () => { selector: 'dt-test-app', template: ` + + + + + + + +
+ {{ t.data }} +
+
`, }) @@ -953,4 +990,24 @@ class EventChartDynamicData { data: 7, }, ]; + + _heatfields = [ + { + start: 0, + end: 35, + color: 'default', + data: 1, + }, + { + start: 45, + end: 65, + color: 'error', + data: 2, + }, + { + start: 65, + color: 'default', + data: 3, + }, + ]; } diff --git a/libs/barista-components/event-chart/src/event-chart.ts b/libs/barista-components/event-chart/src/event-chart.ts index 69780fb443..83d2e7e768 100644 --- a/libs/barista-components/event-chart/src/event-chart.ts +++ b/libs/barista-components/event-chart/src/event-chart.ts @@ -73,20 +73,32 @@ import { DtEventChartEvent, DtEventChartLane, DtEventChartLegendItem, + DtEventChartField, DtEventChartOverlay, + DtEventChartHeatfieldOverlay, DtEventChartSelectedEvent, DT_EVENT_CHART_COLORS, + DtEventChartSelectedField, } from './event-chart-directives'; import { DtEventChartLegend } from './event-chart-legend'; import { dtCreateEventPath } from './merge-and-path/create-event-path'; -import { dtEventChartMergeEvents } from './merge-and-path/merge-events'; -import { RenderEvent } from './render-event.interface'; +import { + dtEventChartMergeEvents, + dtEventChartMergeFields, +} from './merge-and-path/merge-events'; +import { + RenderEvent, + RenderField, + isRenderField, + isRenderEvent, +} from './render-event.interface'; import { DtTimeUnit, DtFormattedValue, formatDuration, } from '@dynatrace/barista-components/formatters'; +const FIELD_BUBBLE_SIZE = 4; const EVENT_BUBBLE_SIZE = 16; const EVENT_BUBBLE_SPACING = 4; @@ -96,6 +108,8 @@ const TICK_WIDTH = 140; const LANE_HEIGHT = EVENT_BUBBLE_SIZE * 3; +const HEATFIELD_OFFSET = 16; + let patternDefsOutlet: PortalOutlet; const OVERLAY_PANEL_CLASS = 'dt-event-chart-overlay-panel'; @@ -144,6 +158,46 @@ const OVERLAY_POSITIONS: ConnectedPosition[] = [ }, ]; +const DT_EVENT_CHART_HEATFIELD_OVERLAY_POSITIONS: ConnectedPosition[] = [ + { + originX: 'center', + originY: 'top', + overlayX: 'center', + overlayY: 'bottom', + offsetY: -8, + }, + { + originX: 'end', + originY: 'top', + overlayX: 'start', + overlayY: 'bottom', + offsetY: -8, + offsetX: 8, + }, + { + originX: 'start', + originY: 'top', + overlayX: 'end', + overlayY: 'bottom', + offsetY: -8, + offsetX: -8, + }, + { + originX: 'end', + originY: 'top', + overlayX: 'start', + overlayY: 'top', + offsetX: 8, + }, + { + originX: 'start', + originY: 'top', + overlayX: 'end', + overlayY: 'top', + offsetX: -8, + }, +]; + @Component({ selector: 'dt-event-chart, dt-sausage-chart', exportAs: 'dtEventChart', @@ -162,10 +216,20 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { // tslint:disable-next-line: no-any private _overlay: TemplateRef; + /** @internal Template reference for the DtEventChart overlay. */ + @ContentChild(DtEventChartHeatfieldOverlay, { read: TemplateRef }) + // tslint:disable-next-line: no-any + private _overlayHeatfield: TemplateRef; + /** @internal Selection of events passed into the DtEventChart via child components. */ @ContentChildren(DtEventChartEvent) _events: QueryList>; + //TODO + /** @internal Selection of events passed into the DtEventChart via child components. */ + @ContentChildren(DtEventChartField) + _heatFields: QueryList>; + /** @internal Selection of lanes passed into the DtEventChart via child components. */ @ContentChildren(DtEventChartLane) _lanes: QueryList; @@ -192,6 +256,8 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { @ViewChild('canvas', { static: true }) _canvasEl: ElementRef; + @ViewChild('container', { static: true }) _container: ElementRef; + /** * @internal * Reference to the rendered lane labels. @@ -214,6 +280,9 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { /** @internal Events that are being rendered in the svgCanvas. */ _renderEvents: RenderEvent[] = []; + /** @internal Events that are being rendered in the svgCanvas. */ + _renderFields: RenderField[] = []; + /** @internal Svg path definition, which connects all renderEvents. */ _renderPath: string | null = null; @@ -235,6 +304,13 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { /** @internal Selected event index. */ _selectedEventIndex: number | undefined; + /** @internal Selected event index. */ + _selectedFieldIndex: number | undefined; + + readonly FIELDS_OFFSET = HEATFIELD_OFFSET; + + hasHeatfields = false; + /** Template portal for the default overlay */ // tslint:disable-next-line: use-default-type-parameter no-any private _portal: TemplatePortal | null; @@ -270,6 +346,14 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { } ngAfterContentInit(): void { + const heatFieldChanges$ = this._heatFields.changes.pipe( + takeUntil(this._destroy$), + switchMap(() => + merge(...this._heatFields.map((e) => e._stateChanges$)).pipe( + startWith(null), + ), + ), + ); // Get all state-changes events from the dt-event-chart-event content children. const eventChanges$ = this._events.changes.pipe( takeUntil(this._destroy$), @@ -302,10 +386,11 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { // Combine all state-changes events from events and lanes to be notified // about all value or configuration changes in the content children. - const contentChanges$ = merge(eventChanges$, laneChanges$).pipe( - startWith(null), - takeUntil(this._destroy$), - ); + const contentChanges$ = merge( + eventChanges$, + laneChanges$, + heatFieldChanges$, + ).pipe(startWith(null), takeUntil(this._destroy$)); // As we need to render the lane labels, which are content children, // before the actual render of the SVG we need make angular recognize @@ -340,21 +425,23 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { } /** - * Select an event based on its index in the event data list. + * Select an event or field based on its index in the event data list. */ - select(index: number): void { + select(index: number, type: 'event' | 'heatfield' = 'event'): void { // If the index is outside the selectable range // do not select anything. if (index < 0 || index > this._events.length) { return; } - this._selectedEventIndex = index; + this._selectedEventIndex = type === 'event' ? index : undefined; + this._selectedFieldIndex = type === 'heatfield' ? index : undefined; this._changeDetectorRef.markForCheck(); } /** Deselect events, which effectively clears the selection on the event chart. */ deselect(): void { this._selectedEventIndex = undefined; + this._selectedFieldIndex = undefined; this._changeDetectorRef.markForCheck(); } @@ -363,13 +450,20 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { this._dismissOverlay(); } - _isSelected(renderEvent: RenderEvent): boolean { + _isSelectedEvent(renderEvent: RenderEvent): boolean { return ( renderEvent.originalIndex === this._selectedEventIndex || (renderEvent.mergedWith || []).includes(this._selectedEventIndex!) ); } + _isSelectedField(renderField: RenderField): boolean { + return ( + renderField.originalIndex === this._selectedFieldIndex || + (renderField.mergedWith || []).includes(this._selectedFieldIndex!) + ); + } + /** @internal Handle the keyDown event on the svg RenderEvent bubble. */ _handleEventKeyDown( keyEvent: KeyboardEvent, @@ -398,6 +492,8 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { // Select the rendered event this._selectedEventIndex = renderEvent.originalIndex; + // Unselect the rendered field + this._selectedFieldIndex = undefined; // Pin the overlay const origin = this.getOriginFromInteractionEvent(event); @@ -412,6 +508,39 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { this._selectedEventIndex = undefined; } + /** + * @internal Emits the data of the selected field and + * registers the field as selected. + */ + _fieldSelected( + field: MouseEvent | KeyboardEvent, + renderField: RenderField, + ): void { + const sources = new DtEventChartSelectedField(renderField.fields); + // If merged events are in the list, the selected field is only triggered on the + // first (and hence displayed) field bubble. + const representingField = this._getRepresentingField(renderField); + representingField.selected.emit(sources); + + // Select the rendered field + this._selectedFieldIndex = renderField.originalIndex; + + // Unselect the rendered field + this._selectedEventIndex = undefined; + + // Pin the overlay + const origin = this.getOriginFromInteractionEvent(field); + this._pinOverlay(origin, renderField); + + // Update the renderEventsArray to force a repaint + this._changeDetectorRef.markForCheck(); + } + + /** @internal Reset the field selection and unpins the overlay */ + _resetFieldSelection(): void { + this._selectedFieldIndex = undefined; + } + /** * @internal * Calculate the svg path for the selected renderEvent. @@ -460,15 +589,110 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { return path.join(' '); } + _calculateFieldOutline( + renderEvent: RenderField, + offset: number = 0, + ): string { + const eventBubbleRadius = FIELD_BUBBLE_SIZE + offset; + const eventBubbleRadiusArcV = FIELD_BUBBLE_SIZE / 2; + const eventBubbleRadiusArcH = eventBubbleRadiusArcV + offset / 2; + const path: string[] = []; + const duration: number = + renderEvent.x2 - renderEvent.x1 - FIELD_BUBBLE_SIZE - 2; + path.push(`M ${renderEvent.x1},${renderEvent.y - FIELD_BUBBLE_SIZE}`); + + path.push(`h${duration}`); + + path.push( + `a${eventBubbleRadiusArcV},${eventBubbleRadiusArcH} 0 0 1 ${eventBubbleRadiusArcV},${eventBubbleRadiusArcH}`, + ); + + path.push(`v${eventBubbleRadius}`); + + path.push( + `a${eventBubbleRadiusArcV},${eventBubbleRadiusArcH} 0 0 1 -${eventBubbleRadiusArcV},${eventBubbleRadiusArcH}`, + ); + + path.push(`h-${duration}`); + + path.push( + `a${eventBubbleRadiusArcV},${eventBubbleRadiusArcH} 0 0 1 -${eventBubbleRadiusArcV},-${eventBubbleRadiusArcH}`, + ); + + path.push(`v-${eventBubbleRadius}`); + + path.push( + `a${eventBubbleRadiusArcV},${eventBubbleRadiusArcH} 0 0 1 ${eventBubbleRadiusArcV},-${eventBubbleRadiusArcH}`, + ); + + path.push('z'); + + return path.join(' '); + } + + _calculateRects(renderEvent: RenderField): string { + const field_bubble_half = FIELD_BUBBLE_SIZE / 2; + const field_bubble_quarter = field_bubble_half / 2; + + const path: string[] = []; + + const y = + this._lanes.length * LANE_HEIGHT - (renderEvent.y - field_bubble_half); + + const duration: number = + renderEvent.x2 - renderEvent.x1 - field_bubble_half * 2; + path.push( + `M ${renderEvent.x1 - field_bubble_half / 2},${ + renderEvent.y - field_bubble_half + }`, + ); + + path.push(`h${duration}`); + + path.push( + `a${field_bubble_quarter},${field_bubble_quarter} 0 0 1 ${field_bubble_quarter},${field_bubble_quarter}`, + ); + + path.push(`v${y}`); + + path.push( + `a${field_bubble_quarter},${field_bubble_quarter} 0 0 1 -${field_bubble_quarter},${field_bubble_quarter}`, + ); + + path.push(`h-${duration}`); + + path.push( + `a${field_bubble_quarter},${field_bubble_quarter} 0 0 1 -${field_bubble_quarter},-${field_bubble_quarter}`, + ); + + path.push(`v-${y}`); + + path.push( + `a${field_bubble_quarter},${field_bubble_quarter} 0 0 1 ${field_bubble_quarter},-${field_bubble_quarter}`, + ); + + path.push('z'); + + return path.join(' '); + } + /** @internal Returns the primary DtEventChartEvent of a list of DtEventChartEvents. */ _getRepresentingEvent(renderEvent: RenderEvent): DtEventChartEvent { return renderEvent.events[0]; } - /** @internal Handles the mouseEnter event on a svg RenderEvent bubble. */ - _handleEventMouseEnter(event: MouseEvent, renderEvent: RenderEvent): void { + /** @internal Returns the primary DtEventChartEvent of a list of DtEventChartEvents. */ + _getRepresentingField(renderEvent: RenderField): DtEventChartField { + return renderEvent.fields[0]; + } + + /** @internal Handles the mouseEnter event or field on a svg RenderEvent bubble. */ + _handleEventMouseEnter( + event: MouseEvent, + renderEvent: RenderEvent | RenderField, + ): void { if (!this._overlayPinned) { - this._createOverlay(); + this._createOverlay(isRenderField(renderEvent)); const origin = this.getOriginFromInteractionEvent(event); this._updateOverlay(origin, renderEvent); } @@ -485,22 +709,31 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { * Track by function for the renderEvents – speeds up performance * and prevent deletion of mouseout */ - _renderEventTrackByFn(index: number, _item: RenderEvent): number { + _renderEventTrackByFn( + index: number, + _item: RenderEvent | RenderField, + ): number { return index; } /** Creates the overlay and attaches it. */ - private _createOverlay(pin: boolean = false): void { + private _createOverlay(isField: boolean, pin: boolean = false): void { // If we do not have an overlay defined, we do not need to attach it - if (!this._overlay) { - return; + if (isField) { + if (!this._overlayHeatfield) { + return; + } + } else { + if (!this._overlay) { + return; + } } // Create the template portal if (!this._portal) { // tslint:disable-next-line: no-any this._portal = new TemplatePortal( - this._overlay, + isField ? this._overlayHeatfield : this._overlay, this._viewContainerRef, { $implicit: [] }, ); @@ -510,6 +743,7 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { panelClass: OVERLAY_PANEL_CLASS, hasBackdrop: pin, backdropClass: 'cdk-overlay-transparent-backdrop', + scrollStrategy: this._overlayService.scrollStrategies.close(), }); this._overlayRef = this._overlayService.create(overlayConfig); this._overlayPinned = pin; @@ -521,6 +755,7 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { .subscribe(() => { this._dismissOverlay(); this._resetEventSelection(); + this._resetFieldSelection(); this._changeDetectorRef.markForCheck(); }); } @@ -540,11 +775,19 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { /** Update the overlay position and the implicit context. */ private _updateOverlay( origin: { x: number; y: number }, - renderEvent: RenderEvent, + renderEvent: RenderEvent | RenderField, ): void { // If there is no overlay defined, we don't need // to run the update call. - if (!this._overlay) { + if (isRenderEvent(renderEvent)) { + if (!this._overlay) { + return; + } + } else if (isRenderField(renderEvent)) { + if (!this._overlayHeatfield) { + return; + } + } else { return; } @@ -552,7 +795,11 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { .position() .flexibleConnectedTo(origin) .setOrigin(origin) - .withPositions(OVERLAY_POSITIONS) + .withPositions( + isRenderField(renderEvent) + ? DT_EVENT_CHART_HEATFIELD_OVERLAY_POSITIONS + : OVERLAY_POSITIONS, + ) .withFlexibleDimensions(true) .withPush(false) .withGrowAfterOpen(true) @@ -560,18 +807,20 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { .withLockedPosition(false); this._overlayRef.updatePositionStrategy(updatedPositionStrategy); - if (this._portal) { - this._portal.context.$implicit = renderEvent.events; + if (this._portal && isRenderEvent(renderEvent)) { + this._portal.context.$implicit = (renderEvent as RenderEvent).events; + } else if (this._portal && isRenderField(renderEvent)) { + this._portal.context.$implicit = (renderEvent as RenderField).fields; } } /** Pins the overlay in place. */ private _pinOverlay( origin: { x: number; y: number }, - renderEvent: RenderEvent, + renderEvent: RenderEvent | RenderField, ): void { this._dismissOverlay(); - this._createOverlay(true); + this._createOverlay(isRenderField(renderEvent), true); this._updateOverlay(origin, renderEvent); } @@ -607,11 +856,14 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { */ private _update(): void { const [min, max] = this._getMinMaxValuesOfEvents(); + this.hasHeatfields = this._heatFields?.length > 0; + // Note: Do not move update calls. // The call order is important! this._updateDimensions(); this._updateRenderLanes(); this._updateRenderEvents(min, max); + this._updateRenderFields(min, max); this._updateRenderPath(); this._updateTicks(min, max); this._changeDetectorRef.detectChanges(); @@ -625,7 +877,10 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { // tslint:disable-next-line: no-magic-numbers this._svgPlotHeight = this._lanes.length * LANE_HEIGHT + 3; // We need to make sure we are in the browser, before updating the dimensions - this._svgHeight = this._svgPlotHeight + TICK_HEIGHT; + this._svgHeight = + this._svgPlotHeight + + TICK_HEIGHT + + (this.hasHeatfields ? HEATFIELD_OFFSET : 0); this._svgWidth = canvasWidth; this._svgViewBox = `0 0 ${canvasWidth} ${this._svgHeight}`; } @@ -698,6 +953,37 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { ); } + /** Updates the field objects that are actually used for rendering. */ + private _updateRenderFields(min: number, max: number): void { + const fields = this._heatFields + .toArray() + .sort((a, b) => (a.start ?? min) - (b.start ?? min)); + const scale = this._getScaleForFields(min, max); + + const renderFields: RenderField[] = []; + for (const field of fields) { + const x1 = scale(field.start ?? min)!; + + const x2 = scale(field.end ?? max)!; + + let color = isValidColor(field.color) ? field.color : 'default'; + + if (isValidColor(field.color)) { + color = field.color; + } + + renderFields.push({ + x1, + x2, + y: FIELD_BUBBLE_SIZE, + color, + fields: [field], + }); + } + + this._renderFields = dtEventChartMergeFields(renderFields, 0); + } + /** Generates and updates the path that connects all the render events. */ private _updateRenderPath(): void { this._renderPath = dtCreateEventPath(this._renderEvents); @@ -738,6 +1024,22 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { ]); } + /** + * Generate a time based scale function based on the field values. + * This scale function should only be used for the time based x-axis. + * Use the _getScaleForEvents for all the other use cases + */ + private _getScaleForFields( + min: number, + max: number, + ): ScaleLinear { + const bubbleRadius = FIELD_BUBBLE_SIZE / 2; + + return scaleLinear() + .domain([min, max]) + .range([bubbleRadius + 1, this._svgWidth]); + } + /** * Generate a time based scale function based on the event values. * This scale function should only be used for the time based x-axis. diff --git a/libs/barista-components/event-chart/src/merge-and-path/merge-events.ts b/libs/barista-components/event-chart/src/merge-and-path/merge-events.ts index 864d73f8a1..c1f4a0cc98 100644 --- a/libs/barista-components/event-chart/src/merge-and-path/merge-events.ts +++ b/libs/barista-components/event-chart/src/merge-and-path/merge-events.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import { RenderEvent } from '../render-event.interface'; +import { RenderEvent, RenderField } from '../render-event.interface'; /** Determines whether two events overlap. */ export function dtEventChartIsOverlappingEvent( // tslint:disable-next-line: no-any - eventA: RenderEvent, + eventA: RenderEvent | RenderField, // tslint:disable-next-line: no-any - eventB: RenderEvent, + eventB: RenderEvent | RenderField, overlapThreshold: number, ): boolean { // Either eventA or eventB have a duration, in which case they should not overlap @@ -32,6 +32,7 @@ export function dtEventChartIsOverlappingEvent( } type MergedRenderEvent = RenderEvent & { merged?: boolean }; +type MergedRenderField = RenderField & { merged?: boolean }; export function dtEventChartMergeEvents( renderEvents: MergedRenderEvent[], @@ -114,3 +115,79 @@ export function dtEventChartMergeEvents( // Filter out the renderEvents that have been merged in the process. return renderEvents.filter((renderEvent) => renderEvent.merged !== true); } + +export function dtEventChartMergeFields( + renderEvents: MergedRenderField[], + overlapThreshold: number, +): RenderField[] { + // Loop over the rendered fields to merge points if necessary. + for (let index = 0; index < renderEvents.length; index += 1) { + const currentEvent: MergedRenderField = renderEvents[index]; + // If the currentEvent is already a merged point + // we should not consider it as new origin point for merging. + if (currentEvent.merged) { + continue; + } + + // From the current point forward, find the merge points that are eligable + // for merge. Points are eligable for merging if: + // - They are on the same lane + // - Have the same color + // - Do not have a duration + // - Overlap with the current point + const mergableEvents = new Map>(); + for ( + let eligableIndex = index + 1; + eligableIndex < renderEvents.length; + eligableIndex += 1 + ) { + const eligableEvent: MergedRenderField = renderEvents[eligableIndex]; + const isOverlapping = dtEventChartIsOverlappingEvent( + currentEvent, + eligableEvent, + overlapThreshold, + ); + const hasSameColor = currentEvent.color === eligableEvent.color; + + // If the lane is the same, but the fields do not overlap, there will be no more overlaps + // after that. We can break the loop right there. + if (!isOverlapping) { + break; + } + + // if there is another colored field on the same lane, do not look any further, as this stops + // the merging chain. + if (!hasSameColor) { + break; + } + + // If the two fields meet all criteria to be merged, record the original index + // and the field. + if (isOverlapping && hasSameColor && eligableEvent.merged !== true) { + mergableEvents.set(eligableIndex, eligableEvent); + } + } + + // Merge the fields in the list. + if (mergableEvents.size > 0) { + currentEvent.mergedWith = []; + for (const [mergedEventIndex, mergedRenderEvent] of Array.from( + mergableEvents.entries(), + )) { + currentEvent.mergedWith.push(mergedEventIndex); + currentEvent.fields = [ + ...currentEvent.fields, + ...mergedRenderEvent.fields, + ]; + mergedRenderEvent.merged = true; + } + } + + // Save the original index of the field to have a reference value between merged + // and not merged elements (originalIndex, mergedWith should be on the same basis). + currentEvent.originalIndex = index; + } + + // Filter out the renderEvents that have been merged in the process. + return renderEvents.filter((renderEvent) => renderEvent.merged !== true); +} diff --git a/libs/barista-components/event-chart/src/render-event.interface.ts b/libs/barista-components/event-chart/src/render-event.interface.ts index 9acd5016f7..7c6aab0657 100644 --- a/libs/barista-components/event-chart/src/render-event.interface.ts +++ b/libs/barista-components/event-chart/src/render-event.interface.ts @@ -17,6 +17,7 @@ import { DtEventChartColors, DtEventChartEvent, + DtEventChartField, } from './event-chart-directives'; /** @@ -33,3 +34,24 @@ export interface RenderEvent { mergedWith?: number[]; originalIndex?: number; } + +/** + * Interface for the RenderField of the Event Chart + */ +export interface RenderField { + x1: number; + x2: number; + y: number; + color: DtEventChartColors; + fields: DtEventChartField[]; + mergedWith?: number[]; + originalIndex?: number; +} + +export const isRenderEvent = (object: RenderEvent | RenderField) => { + return (>object).events !== undefined; +}; + +export const isRenderField = (object: RenderEvent | RenderField) => { + return (>object).fields !== undefined; +}; diff --git a/libs/examples/src/event-chart/BUILD.bazel b/libs/examples/src/event-chart/BUILD.bazel index f5d777311e..48d36881df 100644 --- a/libs/examples/src/event-chart/BUILD.bazel +++ b/libs/examples/src/event-chart/BUILD.bazel @@ -18,6 +18,7 @@ ng_module( "event-chart-legend-example/event-chart-legend-example.html", "event-chart-overlapping-load-example/event-chart-overlapping-load-example.html", "event-chart-overlay-example/event-chart-overlay-example.html", + "event-chart-heatfield-example/event-chart-heatfield-example.html", "event-chart-selection-example/event-chart-selection-example.html", "event-chart-session-replay-example/event-chart-session-replay-example.html", ], diff --git a/libs/examples/src/event-chart/event-chart-examples.module.ts b/libs/examples/src/event-chart/event-chart-examples.module.ts index e45640d209..ef099f3857 100644 --- a/libs/examples/src/event-chart/event-chart-examples.module.ts +++ b/libs/examples/src/event-chart/event-chart-examples.module.ts @@ -26,6 +26,7 @@ import { DtExampleEventChartOverlay } from './event-chart-overlay-example/event- import { DtExampleEventChartSelection } from './event-chart-selection-example/event-chart-selection-example'; import { DtExampleEventChartSessionReplay } from './event-chart-session-replay-example/event-chart-session-replay-example'; import { DtExampleEventChartComplexSelection } from './event-chart-complex-selection-example/event-chart-complex-selection-example'; +import { DtExampleEventChartHeatfield } from './event-chart-heatfield-example/event-chart-heatfield-example'; @NgModule({ imports: [ @@ -40,6 +41,7 @@ import { DtExampleEventChartComplexSelection } from './event-chart-complex-selec DtExampleEventChartLegend, DtExampleEventChartOverlappingLoad, DtExampleEventChartOverlay, + DtExampleEventChartHeatfield, DtExampleEventChartSelection, DtExampleEventChartSessionReplay, DtExampleEventChartComplexSelection, diff --git a/libs/examples/src/event-chart/event-chart-heatfield-example/event-chart-heatfield-example.html b/libs/examples/src/event-chart/event-chart-heatfield-example/event-chart-heatfield-example.html new file mode 100644 index 0000000000..03684e017a --- /dev/null +++ b/libs/examples/src/event-chart/event-chart-heatfield-example/event-chart-heatfield-example.html @@ -0,0 +1,51 @@ + + + + + + + + + + This is the default legend + + + {{ _durationLabel }} + + + This is the error legend + + + +
{{ t.data }}
+
+ +
{{ t.data.name }}
+
+
diff --git a/libs/examples/src/event-chart/event-chart-heatfield-example/event-chart-heatfield-example.ts b/libs/examples/src/event-chart/event-chart-heatfield-example/event-chart-heatfield-example.ts new file mode 100644 index 0000000000..387d2c3478 --- /dev/null +++ b/libs/examples/src/event-chart/event-chart-heatfield-example/event-chart-heatfield-example.ts @@ -0,0 +1,115 @@ +/** + * @license + * Copyright 2021 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component } from '@angular/core'; + +import { DtEventChartSelectedEvent } from '@dynatrace/barista-components/event-chart'; + +@Component({ + selector: 'dt-example-event-chart-heatfield', + templateUrl: 'event-chart-heatfield-example.html', +}) +export class DtExampleEventChartHeatfield { + selected: DtEventChartSelectedEvent; + + _userEventsLaneEnabled = true; + + _errorLegendEnabled = false; + + _durationLabel = 'This is the default duration legend'; + + _events = [ + { + value: 0, + duration: 0, + lane: 'xhr', + color: 'default', + data: 1, + }, + { + value: 15, + duration: 0, + lane: 'xhr', + color: 'default', + data: 2, + }, + { + value: 35, + duration: 0, + lane: 'xhr', + color: 'default', + data: 3, + }, + { + value: 45, + duration: 0, + lane: 'user-event', + color: 'error', + data: 4, + }, + { + value: 65, + duration: 0, + lane: 'user-event', + color: 'conversion', + data: 5, + }, + { + value: 80, + duration: 20, + lane: 'xhr', + color: 'default', + data: 6, + }, + { + value: 110, + duration: 0, + lane: 'xhr', + color: 'default', + data: 7, + }, + ]; + + _heatfields = [ + { + start: 0, + end: 35, + color: 'default', + data: { + name: 'Field 1', + }, + }, + { + start: 45, + end: 65, + color: 'error', + data: { + name: 'Field 2', + }, + }, + { + start: 65, + color: 'default', + data: { + name: 'Field 3', + }, + }, + ]; + + logSelected(selected: DtEventChartSelectedEvent): void { + this.selected = selected; + } +} diff --git a/libs/examples/src/event-chart/index.ts b/libs/examples/src/event-chart/index.ts index 8d37458398..dbc1726a33 100644 --- a/libs/examples/src/event-chart/index.ts +++ b/libs/examples/src/event-chart/index.ts @@ -21,5 +21,6 @@ export * from './event-chart-examples.module'; export * from './event-chart-legend-example/event-chart-legend-example'; export * from './event-chart-overlapping-load-example/event-chart-overlapping-load-example'; export * from './event-chart-overlay-example/event-chart-overlay-example'; +export * from './event-chart-heatfield-example/event-chart-heatfield-example'; export * from './event-chart-selection-example/event-chart-selection-example'; export * from './event-chart-session-replay-example/event-chart-session-replay-example'; diff --git a/libs/examples/src/index.ts b/libs/examples/src/index.ts index ac66611951..354dab8a6c 100644 --- a/libs/examples/src/index.ts +++ b/libs/examples/src/index.ts @@ -133,6 +133,7 @@ import { DtExampleEventChartDefault } from './event-chart/event-chart-default-ex import { DtExampleEventChartLegend } from './event-chart/event-chart-legend-example/event-chart-legend-example'; import { DtExampleEventChartOverlappingLoad } from './event-chart/event-chart-overlapping-load-example/event-chart-overlapping-load-example'; import { DtExampleEventChartOverlay } from './event-chart/event-chart-overlay-example/event-chart-overlay-example'; +import { DtExampleEventChartHeatfield } from './event-chart/event-chart-heatfield-example/event-chart-heatfield-example'; import { DtExampleEventChartSelection } from './event-chart/event-chart-selection-example/event-chart-selection-example'; import { DtExampleEventChartSessionReplay } from './event-chart/event-chart-session-replay-example/event-chart-session-replay-example'; import { DtExampleExpandablePanelDefault } from './expandable-panel/expandable-panel-default-example/expandable-panel-default-example'; @@ -513,6 +514,7 @@ export { DtExampleEventChartLegend, DtExampleEventChartOverlappingLoad, DtExampleEventChartOverlay, + DtExampleEventChartHeatfield, DtExampleEventChartSelection, DtExampleEventChartSessionReplay, DtExampleExpandablePanelDefault, @@ -862,6 +864,7 @@ export const EXAMPLES_MAP = new Map>([ ['DtExampleEventChartLegend', DtExampleEventChartLegend], ['DtExampleEventChartOverlappingLoad', DtExampleEventChartOverlappingLoad], ['DtExampleEventChartOverlay', DtExampleEventChartOverlay], + ['DtExampleEventChartHeatfield', DtExampleEventChartHeatfield], ['DtExampleEventChartSelection', DtExampleEventChartSelection], ['DtExampleEventChartSessionReplay', DtExampleEventChartSessionReplay], ['DtExampleExpandablePanelDefault', DtExampleExpandablePanelDefault],