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 f90ed02f08..83b9daba6e 100644 --- a/libs/barista-components/event-chart/src/event-chart-module.ts +++ b/libs/barista-components/event-chart/src/event-chart-module.ts @@ -28,6 +28,10 @@ import { DtEventChartOverlay, } from './event-chart-directives'; import { DtEventChartLegend } from './event-chart-legend'; +import { + DtFormattersModule, + DtDuration, +} from '@dynatrace/barista-components/formatters'; export const DT_EVENT_CHART_DIRECTIVES = [ DtEventChart, @@ -39,7 +43,8 @@ export const DT_EVENT_CHART_DIRECTIVES = [ @NgModule({ exports: DT_EVENT_CHART_DIRECTIVES, - imports: [CommonModule, DtLegendModule, DtOverlayModule], + imports: [CommonModule, DtLegendModule, DtOverlayModule, DtFormattersModule], declarations: [DtEventChartLegend, ...DT_EVENT_CHART_DIRECTIVES], + providers: [DtDuration], }) export class DtEventChartModule {} 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 5311f0ce6d..7f5366c2bb 100644 --- a/libs/barista-components/event-chart/src/event-chart.spec.ts +++ b/libs/barista-components/event-chart/src/event-chart.spec.ts @@ -25,7 +25,7 @@ import { By } from '@angular/platform-browser'; import { DtEventChartModule } from '@dynatrace/barista-components/event-chart'; import { dispatchFakeEvent } from '@dynatrace/testing/browser'; -import { DtEventChart, roundUp, formatRelativeTimestamp } from './event-chart'; +import { DtEventChart } from './event-chart'; import { DtEventChartSelectedEvent } from './event-chart-directives'; import { DT_UI_TEST_CONFIG, @@ -37,7 +37,7 @@ function getRenderedMergedTextLabels(fixture: ComponentFixture): string[] { const texts = fixture.debugElement.queryAll( By.css('.dt-event-chart-event-mergednumber'), ); - return texts.map(text => text.nativeElement.innerHTML.trim()); + return texts.map((text) => text.nativeElement.innerHTML.trim()); } /** Gets the rendered path that connects the rendered Event bubbles. */ @@ -61,7 +61,7 @@ function getLegendItems( const legendItemElements = fixture.debugElement.queryAll( By.css('.dt-legend-item'), ); - return legendItemElements.map(element => { + return legendItemElements.map((element) => { const label = element .query(By.css('.dt-legend-item-label')) .nativeElement.textContent.trim(); @@ -98,7 +98,7 @@ function getLaneLabels(fixture: ComponentFixture): string[] { const laneLabels = fixture.debugElement.queryAll( By.css('.dt-event-chart-lane-label'), ); - return laneLabels.map(text => text.nativeElement.innerHTML.trim()); + return laneLabels.map((text) => text.nativeElement.innerHTML.trim()); } describe('DtEventChart', () => { @@ -199,7 +199,7 @@ describe('DtEventChart', () => { const splitPath = path .replace(/ ([M,L,C,A])/gim, ' *$1') .split('*') - .map(pathInstruction => pathInstruction.trim()) + .map((pathInstruction) => pathInstruction.trim()) .map((pathInstruction: string): { key: string; x: number; @@ -216,7 +216,7 @@ describe('DtEventChart', () => { }); // Expect instructions to be correct - expect(splitPath.map(e => e.key)).toEqual([ + expect(splitPath.map((e) => e.key)).toEqual([ 'M', 'L', 'L', @@ -674,37 +674,24 @@ describe('DtEventChart', () => { ); expect(selectedEvent).toBeNull(); }); - }); - - describe('x-axis', () => { - describe('formatRelativeTimestamp', () => { - it('should return milliseconds', () => { - expect(formatRelativeTimestamp(10)).toBe('10 ms'); - }); - - it('should return seconds', () => { - expect(formatRelativeTimestamp(10_000)).toBe('10 s'); - }); - - it('should return minutes', () => { - expect(formatRelativeTimestamp(600_000)).toBe('10 min'); - }); - - it('should return hours', () => { - expect(formatRelativeTimestamp(2 * 60 * 60_000)).toBe('2 h'); - }); - - it('should return days', () => { - expect(formatRelativeTimestamp(1.5 * 24 * 60 * 60_000)).toBe('1.5 d'); - }); - }); - describe('roundUp', () => { - it('should return a rounded up number of 2 decimals', () => { - expect(roundUp(1.325, 2)).toBe(1.33); - }); - it('should return less decimals if possible', () => { - expect(roundUp(1, 2)).toBe(1); + describe('x-axis', () => { + it('should have formatted ticks', () => { + const axisLabels = fixture.debugElement + .queryAll(By.css('.dt-event-chart-tick-label')) + .map((el) => el.nativeElement.textContent.trim()); + + expect(axisLabels).toEqual([ + '0 s', + '0.01 s', + '0.02 s', + '0.03 s', + '0.04 s', + '0.05 s', + '0.06 s', + '0.07 s', + '0.08 s', + ]); }); }); }); diff --git a/libs/barista-components/event-chart/src/event-chart.ts b/libs/barista-components/event-chart/src/event-chart.ts index 5676285935..1bd42f630d 100644 --- a/libs/barista-components/event-chart/src/event-chart.ts +++ b/libs/barista-components/event-chart/src/event-chart.ts @@ -81,12 +81,18 @@ 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 { + DtDuration, + DtTimeUnit, + DtFormattedValue, +} from '@dynatrace/barista-components/formatters'; const EVENT_BUBBLE_SIZE = 16; const EVENT_BUBBLE_SPACING = 4; const EVENT_BUBBLE_OVERLAP_THRESHOLD = EVENT_BUBBLE_SIZE / 2; const TICK_HEIGHT = 24; +const TICK_WIDTH = 100; const LANE_HEIGHT = EVENT_BUBBLE_SIZE * 3; @@ -212,7 +218,7 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { _renderPath: string | null = null; /** @internal X axis ticks that are being rendered in the svgCanvas. */ - _renderTicks: { x: number; value: string }[] = []; + _renderTicks: { x: number; value: string | DtFormattedValue }[] = []; /** @internal The width of the svg. */ _svgWidth = 0; @@ -251,6 +257,7 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { private _appRef: ApplicationRef, private _injector: Injector, private _overlayService: Overlay, + private _durationPipe: DtDuration, @Inject(DOCUMENT) private _document: any, private _platform: Platform, /** @breaking-change: `_elementRef` will be mandatory with version 7.0.0 */ @@ -269,14 +276,18 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { const eventChanges$ = this._events.changes.pipe( takeUntil(this._destroy$), switchMap(() => - merge(...this._events.map(e => e._stateChanges$)).pipe(startWith(null)), + merge(...this._events.map((e) => e._stateChanges$)).pipe( + startWith(null), + ), ), ); // Get all state-changes events from the dt-event-chart-lane content children. const laneChanges$ = this._lanes.changes.pipe( takeUntil(this._destroy$), switchMap(() => - merge(...this._lanes.map(e => e._stateChanges$)).pipe(startWith(null)), + merge(...this._lanes.map((e) => e._stateChanges$)).pipe( + startWith(null), + ), ), ); @@ -422,29 +433,31 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { path.push(`M ${renderEvent.x1 - eventBubbleRadius} ${renderEvent.y}`); // Curve up from center to top left path.push( - `A ${eventBubbleRadius} ${eventBubbleRadius} 0 0 1 ${ - renderEvent.x1 - } ${renderEvent.y - eventBubbleRadius}`, + `A ${eventBubbleRadius} ${eventBubbleRadius} 0 0 1 ${renderEvent.x1} ${ + renderEvent.y - eventBubbleRadius + }`, ); // Path the top border path.push(`H ${renderEvent.x2}`); // Curve to down from top right to center path.push( - `A ${eventBubbleRadius} ${eventBubbleRadius} 0 0 1 ${renderEvent.x2 + - eventBubbleRadius} ${renderEvent.y}`, + `A ${eventBubbleRadius} ${eventBubbleRadius} 0 0 1 ${ + renderEvent.x2 + eventBubbleRadius + } ${renderEvent.y}`, ); // Curve to down from center to bottom right path.push( - `A ${eventBubbleRadius} ${eventBubbleRadius} 0 0 1 ${ - renderEvent.x2 - } ${renderEvent.y + eventBubbleRadius}`, + `A ${eventBubbleRadius} ${eventBubbleRadius} 0 0 1 ${renderEvent.x2} ${ + renderEvent.y + eventBubbleRadius + }`, ); // Path the bottom border path.push(`H ${renderEvent.x1}`); // Curve to up from bottom left to center path.push( - `A ${eventBubbleRadius} ${eventBubbleRadius} 0 0 1 ${renderEvent.x1 - - eventBubbleRadius} ${renderEvent.y}`, + `A ${eventBubbleRadius} ${eventBubbleRadius} 0 0 1 ${ + renderEvent.x1 - eventBubbleRadius + } ${renderEvent.y}`, ); return path.join(' '); } @@ -624,7 +637,7 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { private _updateRenderLanes(): void { let y = 1; this._renderLanes = this._lanes - .map(lane => { + .map((lane) => { const renderLane = { lane, y }; y += LANE_HEIGHT + 1; return renderLane; @@ -695,14 +708,16 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { /** Generates and updates the ticks for the x-axis. */ private _updateTicks(min: number, max: number): void { const timeScale = this._getTimeScaleForEvents(min, max); - const dateTicks = timeScale.ticks(); - this._renderTicks = dateTicks.map(date => { + + const tickAmount = Math.floor( + (timeScale.range()[1] - timeScale.range()[0]) / TICK_WIDTH, + ); + const dateTicks = timeScale.ticks(tickAmount); + this._renderTicks = dateTicks.map((date) => { const timestamp = date.getTime(); return { x: timeScale(timestamp), - // TODO @thomas.pink, @thomas.heller: - // Investigate if we should move the time formatting to a pipe - value: formatRelativeTimestamp(timestamp), + value: this._formatRelativeTimestamp(timestamp), }; }); } @@ -765,7 +780,7 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { /** Tries to find a lane base on a provided name. */ private _getLaneByName(name: string): DtEventChartLane | null { - return this._lanes.find(lane => lane.name === name) || null; + return this._lanes.find((lane) => lane.name === name) || null; } /** Returns the position (reversed index) of a lane. */ @@ -803,6 +818,31 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { patternDefsOutlet = outlet; } + + /** Formats a relative timestamp into a readable text. */ + private _formatRelativeTimestamp( + timestamp: number, + ): string | DtFormattedValue { + // TODO: once duration pipe returns a nice value maybe the unit is automatically chosen + let outputUnit = DtTimeUnit.SECOND; + + const sec = 1000; + const min = sec * 60; + const hour = min * 60; + const day = hour * 24; + + if (timestamp >= day) { + outputUnit = DtTimeUnit.DAY; + } else if (timestamp >= hour) { + outputUnit = DtTimeUnit.HOUR; + } else if (timestamp >= min) { + outputUnit = DtTimeUnit.MINUTE; + } else if (timestamp >= sec) { + outputUnit = DtTimeUnit.SECOND; + } + + return this._durationPipe.transform(timestamp, 'PRECISE', outputUnit); + } } /** Determines if a passed parameter is part of the DtEventChartColors. */ @@ -810,32 +850,3 @@ export class DtEventChart implements AfterContentInit, OnInit, OnDestroy { function isValidColor(color: any): color is DtEventChartColors { return isDefined(color) && DT_EVENT_CHART_COLORS.indexOf(color) !== -1; } - -/** Formats a relative timestamp into a readable text. */ -export function formatRelativeTimestamp(timestamp: number): string { - const sec = 1000; - const min = sec * 60; - const hour = min * 60; - const day = hour * 24; - const decimals = 2; - if (timestamp >= day) { - return `${roundUp(timestamp / day, decimals)} d`; - } else if (timestamp >= hour) { - return `${roundUp(timestamp / hour, decimals)} h`; - } else if (timestamp >= min) { - return `${roundUp(timestamp / min, decimals)} min`; - } else if (timestamp >= sec) { - return `${roundUp(timestamp / sec, decimals)} s`; - } - return `${timestamp} ms`; -} - -/** - * Rounds the given number to the specified decimals. - * @param num number to be rounded - * @param decimals maximum amount of decimals - * @returns rounded number - */ -export function roundUp(num: number, decimals: number): number { - return Math.round(10 ** decimals * num) / 10 ** decimals; -}