Skip to content

Commit d02e165

Browse files
feat: log marker indicator in the waterfall view (#899)
* feat: log marker indicator in the waterfall view * fix: addressing review comments * fix: addressing review comments * fix: errors after merge * fix: lint errors * fix: addressed review comments * fix: addressed review comments * fix: using summary instead of attributes * fix: addressed review comments * fix: test cases
1 parent 83db9d9 commit d02e165

18 files changed

+511
-42
lines changed

projects/components/src/public-api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ export * from './select/select.component';
238238
export * from './select/select.module';
239239

240240
// Sequence
241-
export { SequenceSegment } from './sequence/sequence';
241+
export { Marker, MarkerDatum, SequenceSegment } from './sequence/sequence';
242242
export * from './sequence/sequence-chart.component';
243243
export * from './sequence/sequence-chart.module';
244244

projects/components/src/sequence/renderer/sequence-bar-renderer.service.ts

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
1-
import { Injectable } from '@angular/core';
1+
import { ElementRef, Injectable } from '@angular/core';
22
import { ScaleLinear } from 'd3-scale';
33
import { BaseType, select, Selection } from 'd3-selection';
44
import { SequenceChartAxisService } from '../axis/sequence-chart-axis.service';
5-
import { SequenceLayoutStyleClass, SequenceOptions, SequenceSegment, SequenceSVGSelection } from '../sequence';
5+
import { Marker, SequenceLayoutStyleClass, SequenceOptions, SequenceSegment, SequenceSVGSelection } from '../sequence';
66
import { SequenceObject } from '../sequence-object';
77

88
@Injectable()
99
export class SequenceBarRendererService {
1010
private static readonly DATA_ROW_CLASS: string = 'data-row';
1111
private static readonly HOVERED_ROW_CLASS: string = 'hovered-row';
1212
private static readonly SELECTED_ROW_CLASS: string = 'selected-row';
13+
private static readonly MARKER_CLASS: string = 'marker';
14+
private static readonly MARKERS_CLASS: string = 'markers';
1315
private static readonly BACKDROP_CLASS: string = 'backdrop';
1416
private static readonly BACKDROP_BORDER_TOP_CLASS: string = 'backdrop-border-top';
1517
private static readonly BACKDROP_BORDER_BOTTOM_CLASS: string = 'backdrop-border-bottom';
1618

19+
private readonly markerWidth: number = 2;
20+
1721
public constructor(private readonly sequenceChartAxisService: SequenceChartAxisService) {}
1822

1923
public drawBars(chartSelection: SequenceSVGSelection, options: SequenceOptions): void {
@@ -39,9 +43,11 @@ export class SequenceBarRendererService {
3943

4044
this.drawBackdropRect(transformedBars, options, plotWidth);
4145
this.drawBarValueRect(transformedBars, xScale, options);
46+
this.drawBarMarkers(transformedBars, xScale, options);
4247
this.drawBarValueText(transformedBars, xScale, options);
4348
this.setupHoverListener(transformedBars, options);
4449
this.setupClickListener(transformedBars, options);
50+
this.setupMarkerHoverListener(transformedBars, options);
4551
this.updateDataRowHover(chartSelection, options);
4652
this.updateDataRowSelection(chartSelection, options);
4753
}
@@ -108,6 +114,23 @@ export class SequenceBarRendererService {
108114
.classed('bar-value', true);
109115
}
110116

117+
private drawBarMarkers(
118+
transformedBars: TransformedBarSelection,
119+
xScale: ScaleLinear<number, number>,
120+
options: SequenceOptions
121+
): void {
122+
transformedBars
123+
.selectAll(`g.${SequenceBarRendererService.MARKERS_CLASS}`)
124+
.data(segment => this.getGroupedMarkers(segment, xScale))
125+
.enter()
126+
.append('rect')
127+
.classed(`${SequenceBarRendererService.MARKER_CLASS}`, true)
128+
.attr('transform', dataRow => `translate(${dataRow.markerTime},${(options.rowHeight - options.barHeight) / 2})`)
129+
.attr('width', this.markerWidth)
130+
.attr('height', 12)
131+
.style('fill', 'white');
132+
}
133+
111134
private drawBarValueText(
112135
transformedBars: TransformedBarSelection,
113136
xScale: ScaleLinear<number, number>,
@@ -175,6 +198,49 @@ export class SequenceBarRendererService {
175198
options.selected = options.selected === dataRow ? undefined : dataRow;
176199
options.onSegmentSelected(dataRow);
177200
}
201+
202+
private setupMarkerHoverListener(transformedBars: TransformedBarSelection, options: SequenceOptions): void {
203+
transformedBars
204+
.selectAll<SVGRectElement, Marker>(`rect.${SequenceBarRendererService.MARKER_CLASS}`)
205+
.on('mouseenter', (dataRow, index, nodes) => {
206+
options.onMarkerHovered({ marker: dataRow, origin: new ElementRef(nodes[index]) });
207+
});
208+
}
209+
210+
private getGroupedMarkers(segment: SequenceSegment, xScale: ScaleLinear<number, number>): Marker[] {
211+
const scaledStart: number = Math.floor(xScale(segment.start)!);
212+
const scaledEnd: number = Math.floor(xScale(segment.end)!);
213+
const pixelScaledMarkers: Marker[] = segment.markers.map((marker: Marker) => ({
214+
...marker,
215+
markerTime: Math.floor(xScale(marker.markerTime)!)
216+
}));
217+
const scaledNormalizedMarkers: Marker[] = [];
218+
let markerTime = -1 * Infinity;
219+
let index = -1;
220+
pixelScaledMarkers.forEach((marker: Marker) => {
221+
// For 1px gap
222+
if (marker.markerTime >= markerTime + this.markerWidth + 1) {
223+
index++;
224+
scaledNormalizedMarkers.push({
225+
...marker,
226+
markerTime:
227+
marker.markerTime <= scaledStart + this.markerWidth // Grouping - closest to start
228+
? scaledStart + this.markerWidth + 1
229+
: marker.markerTime >= scaledEnd - this.markerWidth // Grouping - closest to end
230+
? scaledEnd - this.markerWidth - 2
231+
: marker.markerTime
232+
});
233+
markerTime = scaledNormalizedMarkers[index].markerTime;
234+
} else {
235+
scaledNormalizedMarkers[index] = {
236+
...scaledNormalizedMarkers[index],
237+
timestamps: [...scaledNormalizedMarkers[index].timestamps, ...marker.timestamps]
238+
};
239+
}
240+
});
241+
242+
return scaledNormalizedMarkers;
243+
}
178244
}
179245

180246
type TransformedBarSelection = Selection<BaseType, SequenceSegment, SVGElement, SequenceObject>;

projects/components/src/sequence/sequence-chart.component.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@
5454
}
5555
}
5656

57+
.marker {
58+
cursor: pointer;
59+
}
60+
5761
.hovered-row {
5862
.backdrop {
5963
fill-opacity: 100;

projects/components/src/sequence/sequence-chart.component.test.ts

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,28 +24,32 @@ describe('Sequence Chart component', () => {
2424
start: 1569357460346,
2525
end: 1569357465346,
2626
label: 'Segment 1',
27-
color: 'blue'
27+
color: 'blue',
28+
markers: []
2829
},
2930
{
3031
id: '2',
3132
start: 1569357461346,
3233
end: 1569357465346,
3334
label: 'Segment 2',
34-
color: 'green'
35+
color: 'green',
36+
markers: []
3537
},
3638
{
3739
id: '3',
3840
start: 1569357462346,
3941
end: 1569357465346,
4042
label: 'Segment 3',
41-
color: 'green'
43+
color: 'green',
44+
markers: []
4245
},
4346
{
4447
id: '4',
4548
start: 1569357463346,
4649
end: 1569357465346,
4750
label: 'Segment 4',
48-
color: 'green'
51+
color: 'green',
52+
markers: []
4953
}
5054
];
5155
const chart = createHost(`<ht-sequence-chart [data]="data"></ht-sequence-chart>`, {
@@ -91,7 +95,8 @@ describe('Sequence Chart component', () => {
9195
start: 1569357460346,
9296
end: 1569357465346,
9397
label: 'Segment 1',
94-
color: 'blue'
98+
color: 'blue',
99+
markers: []
95100
}
96101
];
97102
const chart = createHost(`<ht-sequence-chart [data]="data" [unit]="unit"></ht-sequence-chart>`, {
@@ -117,7 +122,8 @@ describe('Sequence Chart component', () => {
117122
start: 1569357460346,
118123
end: 1569357465346,
119124
label: 'Segment 1',
120-
color: 'blue'
125+
color: 'blue',
126+
markers: []
121127
}
122128
];
123129
const chart = createHost(`<ht-sequence-chart [data]="data" [rowHeight]="rowHeight"></ht-sequence-chart>`, {
@@ -132,14 +138,46 @@ describe('Sequence Chart component', () => {
132138
expect(dataRow!.getAttribute('height')).toEqual('50');
133139
});
134140

141+
test('should render marker with correct width and height', () => {
142+
const segmentsData = [
143+
{
144+
id: '1',
145+
start: 1569357460346,
146+
end: 1569357465346,
147+
label: 'Segment 1',
148+
color: 'blue',
149+
markers: [
150+
{
151+
id: '1',
152+
markerTime: 1569357460347,
153+
timestamps: ['1569357460347']
154+
}
155+
]
156+
}
157+
];
158+
const chart = createHost(`<ht-sequence-chart [data]="data"></ht-sequence-chart>`, {
159+
hostProps: {
160+
data: segmentsData,
161+
rowHeight: 50
162+
}
163+
});
164+
165+
const markers = chart.queryAll('.marker', { root: true });
166+
expect(markers.length).toBe(1);
167+
const markerRect = select(markers[0]);
168+
expect(markerRect.attr('width')).toBe('2');
169+
expect(markerRect.attr('height')).toBe('12');
170+
});
171+
135172
test('should render with correct bar height', () => {
136173
const segmentsData = [
137174
{
138175
id: '1',
139176
start: 1569357460346,
140177
end: 1569357465346,
141178
label: 'Segment 1',
142-
color: 'blue'
179+
color: 'blue',
180+
markers: []
143181
}
144182
];
145183
const chart = createHost(`<ht-sequence-chart [data]="data" [barHeight]="barHeight"></ht-sequence-chart>`, {
@@ -164,7 +202,8 @@ describe('Sequence Chart component', () => {
164202
start: 1569357460346,
165203
end: 1569357465346,
166204
label: 'Segment 1',
167-
color: 'blue'
205+
color: 'blue',
206+
markers: []
168207
}
169208
];
170209
const chart = createHost(`<ht-sequence-chart [data]="data" [headerHeight]="headerHeight"></ht-sequence-chart>`, {
@@ -186,14 +225,16 @@ describe('Sequence Chart component', () => {
186225
start: 1569357460346,
187226
end: 1569357465346,
188227
label: 'Segment 1',
189-
color: 'blue'
228+
color: 'blue',
229+
markers: []
190230
},
191231
{
192232
id: '2',
193233
start: 1569357460346,
194234
end: 1569357465346,
195235
label: 'Segment 2',
196-
color: 'green'
236+
color: 'green',
237+
markers: []
197238
}
198239
];
199240
const chart = createHost(`<ht-sequence-chart [data]="data" [hovered]="hovered"></ht-sequence-chart>`, {
@@ -230,14 +271,16 @@ describe('Sequence Chart component', () => {
230271
start: 1569357460346,
231272
end: 1569357465346,
232273
label: 'Segment 1',
233-
color: 'blue'
274+
color: 'blue',
275+
markers: []
234276
},
235277
{
236278
id: '2',
237279
start: 1569357460346,
238280
end: 1569357465346,
239281
label: 'Segment 2',
240-
color: 'green'
282+
color: 'green',
283+
markers: []
241284
}
242285
];
243286
const chart = createHost(`<ht-sequence-chart [data]="data" [selection]="selection"></ht-sequence-chart>`, {

projects/components/src/sequence/sequence-chart.component.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
} from '@angular/core';
1111
import { RecursivePartial, TypedSimpleChanges } from '@hypertrace/common';
1212
import { minBy } from 'lodash-es';
13-
import { SequenceOptions, SequenceSegment } from './sequence';
13+
import { Marker, MarkerDatum, SequenceOptions, SequenceSegment } from './sequence';
1414
import { SequenceChartService } from './sequence-chart.service';
1515
import { SequenceObject } from './sequence-object';
1616

@@ -52,6 +52,9 @@ export class SequenceChartComponent implements OnChanges {
5252
SequenceSegment | undefined
5353
>();
5454

55+
@Output()
56+
public readonly markerHoveredChange: EventEmitter<MarkerDatum> = new EventEmitter<MarkerDatum>();
57+
5558
@ViewChild('chartContainer', { static: true })
5659
private readonly chartContainer!: ElementRef;
5760

@@ -100,7 +103,8 @@ export class SequenceChartComponent implements OnChanges {
100103
barHeight: this.barHeight,
101104
unit: this.unit,
102105
onSegmentSelected: (segment?: SequenceSegment) => this.onSegmentSelected(segment),
103-
onSegmentHovered: (segment?: SequenceSegment) => this.onSegmentHovered(segment)
106+
onSegmentHovered: (segment?: SequenceSegment) => this.onSegmentHovered(segment),
107+
onMarkerHovered: (datum?: MarkerDatum) => this.onMarkerHovered(datum)
104108
};
105109
}
106110

@@ -115,7 +119,10 @@ export class SequenceChartComponent implements OnChanges {
115119
id: segment.id,
116120
start: segment.start - minStart,
117121
end: segment.end - minStart,
118-
color: segment.color
122+
color: segment.color,
123+
markers: segment.markers
124+
.map((marker: Marker) => ({ ...marker, markerTime: marker.markerTime - minStart }))
125+
.sort((marker1, marker2) => marker1.markerTime - marker2.markerTime)
119126
}));
120127
}
121128

@@ -128,4 +135,8 @@ export class SequenceChartComponent implements OnChanges {
128135
this.hovered = segment;
129136
this.hoveredChange.emit(segment);
130137
}
138+
139+
private onMarkerHovered(datum?: MarkerDatum): void {
140+
this.markerHoveredChange.emit(datum);
141+
}
131142
}

projects/components/src/sequence/sequence-chart.service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ export class SequenceChartService {
101101
},
102102
onSegmentHovered: () => {
103103
/** NOOP */
104+
},
105+
onMarkerHovered: () => {
106+
/** NOOP */
104107
}
105108
};
106109
}

projects/components/src/sequence/sequence.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ElementRef } from '@angular/core';
12
import { Selection } from 'd3-selection';
23
import { SequenceObject } from './sequence-object';
34

@@ -6,6 +7,18 @@ export interface SequenceSegment {
67
start: number;
78
end: number;
89
color: string;
10+
markers: Marker[];
11+
}
12+
13+
export interface Marker {
14+
id: string;
15+
markerTime: number;
16+
timestamps: string[];
17+
}
18+
19+
export interface MarkerDatum {
20+
marker: Marker;
21+
origin: ElementRef;
922
}
1023

1124
/* Internal Types */
@@ -20,6 +33,7 @@ export interface SequenceOptions {
2033
unit: string | undefined;
2134
onSegmentSelected(row?: SequenceSegment): void;
2235
onSegmentHovered(row?: SequenceSegment): void;
36+
onMarkerHovered(datum?: MarkerDatum): void;
2337
}
2438

2539
export interface MarginOptions {

0 commit comments

Comments
 (0)