Merge pull request #68495 from rhcs-dashboard/76169-inconsistant-tooltip-timestamp-format-performance-chart

Reviewed-by: Afreen Misbah <afreen@ibm.com>
This commit is contained in:
Afreen Misbah 2026-07-22 13:25:57 +05:30 committed by GitHub
commit 7b29dcc7e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 67 additions and 38 deletions

View File

@ -14,6 +14,7 @@ const jestConfig = {
'~/(.*)$': '<rootDir>/src/$1',
'^@carbon/icons/es/(.*)$': '@carbon/icons/lib/$1.js',
'^lodash-es$': 'lodash',
'^@carbon/charts$': '<rootDir>/node_modules/@carbon/charts/dist/index.mjs'
},
moduleFileExtensions: ['ts', 'html', 'js', 'json', 'mjs', 'cjs'],
preset: 'jest-preset-angular',

View File

@ -10,7 +10,6 @@ describe('AreaChartComponent', () => {
let component: AreaChartComponent;
let fixture: ComponentFixture<AreaChartComponent>;
let numberFormatterService: NumberFormatterService;
let datePipe: DatePipe;
const mockData: ChartPoint[] = [
{
@ -41,23 +40,15 @@ describe('AreaChartComponent', () => {
unitlessLabels: ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
};
const datePipeMock = {
transform: jest.fn().mockReturnValue('01 Jan, 00:00:00')
};
await TestBed.configureTestingModule({
imports: [ChartsModule, AreaChartComponent],
providers: [
{ provide: NumberFormatterService, useValue: numberFormatterMock },
{ provide: DatePipe, useValue: datePipeMock }
],
providers: [DatePipe, { provide: NumberFormatterService, useValue: numberFormatterMock }],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents();
fixture = TestBed.createComponent(AreaChartComponent);
component = fixture.componentInstance;
numberFormatterService = TestBed.inject(NumberFormatterService);
datePipe = TestBed.inject(DatePipe);
});
it('should mount', () => {
@ -133,23 +124,36 @@ describe('AreaChartComponent', () => {
expect(component.chartOptions?.tooltip?.enabled).toBe(true);
});
it('should format tooltip with custom date format', () => {
const testDate = new Date('2024-01-01T12:30:45Z');
const formattedDate = '01 Jan, 12:30:45';
const defaultHTML = '<div><p class="value">2024-01-01T12:30:45Z</p></div>';
it('should wire tooltip customHTML with arity 2', () => {
component.chartTitle = 'Test Chart';
component.dataUnit = 'B/s';
component.rawData = mockData;
(datePipe.transform as jest.Mock).mockReturnValue(formattedDate);
(numberFormatterService.formatFromTo as jest.Mock).mockReturnValue('4.00 KiB/s');
const result = component.formatChartTooltip(defaultHTML, [{ date: testDate }]);
fixture.detectChanges();
component.ngOnChanges({
rawData: new SimpleChange(null, mockData, false)
});
expect(datePipe.transform).toHaveBeenCalledWith(testDate, 'dd MMM, HH:mm:ss');
expect(result).toContain(formattedDate);
expect(result).not.toContain('2024-01-01T12:30:45Z');
expect(component.chartOptions?.tooltip?.customHTML?.length).toBe(2);
});
it('should return default HTML if tooltip data is empty', () => {
it('should replace x-value label with Time in tooltip HTML', () => {
const defaultHTML =
'<ul class="multi-tooltip"><li><div class="datapoint-tooltip">' +
'<div class="label"><p>x-value</p></div><p class="value">ignored</p></div></li></ul>';
const result = component.formatChartTooltip(defaultHTML);
expect(result).toContain('<p>Time</p>');
expect(result).not.toContain('x-value');
expect(result).toContain('<p class="value">ignored</p>');
});
it('should return default HTML if tooltip has no x-value label', () => {
const defaultHTML = '<div><p>Default</p></div>';
const result = component.formatChartTooltip(defaultHTML, []);
const result = component.formatChartTooltip(defaultHTML);
expect(result).toBe(defaultHTML);
});

View File

@ -13,11 +13,11 @@ import {
ChartTabularData,
ToolbarControlTypes,
ScaleTypes,
ChartsModule
ChartsModule,
TickRotations
} from '@carbon/charts-angular';
import merge from 'lodash.merge';
import { NumberFormatterService } from '../../services/number-formatter.service';
import { DatePipe } from '@angular/common';
import { ChartPoint } from '../../models/area-chart-point';
import {
DECIMAL,
@ -26,6 +26,12 @@ import {
getDivisor,
getLabels
} from '../../helpers/unit-format-utils';
import { DatePipe } from '@angular/common';
const DEFAULT_TICKS_COUNT = 4;
const FIVE_MINUTE_SPAN_SECONDS = 300;
const TIME_SPAN_TOLERANCE_SECONDS = 30;
const TOOLTIP_TIME_FORMAT = 'MMM d, hh:mm a';
@Component({
selector: 'cd-area-chart',
@ -59,9 +65,10 @@ export class AreaChartComponent implements OnChanges {
private cdr = inject(ChangeDetectorRef);
private numberFormatter: NumberFormatterService = inject(NumberFormatterService);
private datePipe = inject(DatePipe);
private lastEmittedRawValues?: Record<string, number>;
private datePipe = inject(DatePipe);
ngOnChanges(changes: SimpleChanges): void {
if (changes['rawData'] && this.rawData?.length) {
this.updateChart();
@ -129,7 +136,22 @@ export class AreaChartComponent implements OnChanges {
this.lastEmittedRawValues = { ...latestEntry.values };
}
// Calculate time span in seconds from raw data timestamps
private getTimeSpanInSeconds(): number {
if (!this.rawData || this.rawData.length < 2) {
return 0;
}
const firstTimestamp = new Date(this.rawData[0].timestamp).getTime();
const lastTimestamp = new Date(this.rawData[this.rawData.length - 1].timestamp).getTime();
return (lastTimestamp - firstTimestamp) / 1000;
}
private getChartOptions(max: number, labels: string[], divisor: number): AreaChartOptions {
const timeSpan = this.getTimeSpanInSeconds();
const isFiveMinuteSpan =
timeSpan > 0 &&
timeSpan >= FIVE_MINUTE_SPAN_SECONDS - TIME_SPAN_TOLERANCE_SECONDS &&
timeSpan <= FIVE_MINUTE_SPAN_SECONDS + TIME_SPAN_TOLERANCE_SECONDS;
return {
legend: {
enabled: this.legendEnabled
@ -139,8 +161,8 @@ export class AreaChartComponent implements OnChanges {
mapsTo: 'date',
scaleType: ScaleTypes.TIME,
ticks: {
number: 4,
rotateIfSmallerThan: 0
number: DEFAULT_TICKS_COUNT,
rotation: isFiveMinuteSpan ? TickRotations.ALWAYS : TickRotations.AUTO
}
},
left: {
@ -161,9 +183,18 @@ export class AreaChartComponent implements OnChanges {
tooltip: {
enabled: true,
showTotal: false,
valueFormatter: (value: number): string =>
(this.formatValueForChart(value, labels, divisor, this.decimals) || value).toString(),
customHTML: (data, defaultHTML) => this.formatChartTooltip(defaultHTML, data)
truncation: {
type: 'none'
},
valueFormatter: (value: number | Date, label: string): string => {
if (value instanceof Date && label === 'x-value') {
return this.datePipe.transform(value, TOOLTIP_TIME_FORMAT) ?? '';
}
return (
this.formatValueForChart(value, labels, divisor, this.decimals) || value
).toString();
},
customHTML: (_data, defaultHTML) => this.formatChartTooltip(defaultHTML)
},
points: {
enabled: false
@ -193,15 +224,8 @@ export class AreaChartComponent implements OnChanges {
};
}
// Custom tooltip formatter to replace default timestamp with a formatted one.
formatChartTooltip(defaultHTML: string, data: { date: Date }[]): string {
if (!data?.length) return defaultHTML;
const formattedTime = this.datePipe.transform(data[0].date, 'dd MMM, HH:mm:ss');
return defaultHTML.replace(
/<p class="value">.*?<\/p>/,
`<p class="value">${formattedTime}</p>`
);
formatChartTooltip(defaultHTML: string): string {
return defaultHTML.replace(/<p>x-value<\/p>/i, `<p>${$localize`Time`}</p>`);
}
// Uses number formatter service to convert chart value based on unit and divisor.