Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion projects/components/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ export * from './select/select.component';
export * from './select/select.module';

// Sequence
export { SequenceSegment } from './sequence/sequence';
export { Marker, MarkerDatum, SequenceSegment } from './sequence/sequence';
export * from './sequence/sequence-chart.component';
export * from './sequence/sequence-chart.module';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { Injectable } from '@angular/core';
import { ElementRef, Injectable } from '@angular/core';
import { ScaleLinear } from 'd3-scale';
import { BaseType, select, Selection } from 'd3-selection';
import { SequenceChartAxisService } from '../axis/sequence-chart-axis.service';
import { SequenceLayoutStyleClass, SequenceOptions, SequenceSegment, SequenceSVGSelection } from '../sequence';
import { Marker, SequenceLayoutStyleClass, SequenceOptions, SequenceSegment, SequenceSVGSelection } from '../sequence';
import { SequenceObject } from '../sequence-object';

@Injectable()
export class SequenceBarRendererService {
private static readonly DATA_ROW_CLASS: string = 'data-row';
private static readonly HOVERED_ROW_CLASS: string = 'hovered-row';
private static readonly SELECTED_ROW_CLASS: string = 'selected-row';
private static readonly MARKER_CLASS: string = 'marker';
private static readonly MARKERS_CLASS: string = 'markers';
private static readonly BACKDROP_CLASS: string = 'backdrop';
private static readonly BACKDROP_BORDER_TOP_CLASS: string = 'backdrop-border-top';
private static readonly BACKDROP_BORDER_BOTTOM_CLASS: string = 'backdrop-border-bottom';

private readonly markerWidth: number = 2;

public constructor(private readonly sequenceChartAxisService: SequenceChartAxisService) {}

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

this.drawBackdropRect(transformedBars, options, plotWidth);
this.drawBarValueRect(transformedBars, xScale, options);
this.drawBarMarkers(transformedBars, xScale, options);
this.drawBarValueText(transformedBars, xScale, options);
this.setupHoverListener(transformedBars, options);
this.setupClickListener(transformedBars, options);
this.setupMarkerHoverListener(transformedBars, options);
this.updateDataRowHover(chartSelection, options);
this.updateDataRowSelection(chartSelection, options);
}
Expand Down Expand Up @@ -108,6 +114,23 @@ export class SequenceBarRendererService {
.classed('bar-value', true);
}

private drawBarMarkers(
transformedBars: TransformedBarSelection,
xScale: ScaleLinear<number, number>,
options: SequenceOptions
): void {
transformedBars
.selectAll(`g.${SequenceBarRendererService.MARKERS_CLASS}`)
.data(segment => this.getGroupedMarkers(segment, xScale))
.enter()
.append('rect')
.classed(`${SequenceBarRendererService.MARKER_CLASS}`, true)
.attr('transform', dataRow => `translate(${dataRow.markerTime},${(options.rowHeight - options.barHeight) / 2})`)
.attr('width', this.markerWidth)
.attr('height', 12)
.style('fill', 'white');
}

private drawBarValueText(
transformedBars: TransformedBarSelection,
xScale: ScaleLinear<number, number>,
Expand Down Expand Up @@ -175,6 +198,49 @@ export class SequenceBarRendererService {
options.selected = options.selected === dataRow ? undefined : dataRow;
options.onSegmentSelected(dataRow);
}

private setupMarkerHoverListener(transformedBars: TransformedBarSelection, options: SequenceOptions): void {
transformedBars
.selectAll<SVGRectElement, Marker>(`rect.${SequenceBarRendererService.MARKER_CLASS}`)
.on('mouseenter', (dataRow, index, nodes) => {
options.onMarkerHovered({ marker: dataRow, origin: new ElementRef(nodes[index]) });
});
}

private getGroupedMarkers(segment: SequenceSegment, xScale: ScaleLinear<number, number>): Marker[] {
const scaledStart: number = Math.floor(xScale(segment.start)!);
const scaledEnd: number = Math.floor(xScale(segment.end)!);
const pixelScaledMarkers: Marker[] = segment.markers.map((marker: Marker) => ({
...marker,
markerTime: Math.floor(xScale(marker.markerTime)!)
}));
const scaledNormalizedMarkers: Marker[] = [];
let markerTime = -1 * Infinity;
let index = -1;
pixelScaledMarkers.forEach((marker: Marker) => {
Comment thread
itssharmasandeep marked this conversation as resolved.
// For 1px gap
if (marker.markerTime >= markerTime + this.markerWidth + 1) {
index++;
scaledNormalizedMarkers.push({
...marker,
markerTime:
marker.markerTime <= scaledStart + this.markerWidth // Grouping - closest to start
? scaledStart + this.markerWidth + 1
: marker.markerTime >= scaledEnd - this.markerWidth // Grouping - closest to end
? scaledEnd - this.markerWidth - 2
: marker.markerTime
});
markerTime = scaledNormalizedMarkers[index].markerTime;
} else {
scaledNormalizedMarkers[index] = {
...scaledNormalizedMarkers[index],
timestamps: [...scaledNormalizedMarkers[index].timestamps, ...marker.timestamps]
};
}
});

return scaledNormalizedMarkers;
}
}

type TransformedBarSelection = Selection<BaseType, SequenceSegment, SVGElement, SequenceObject>;
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@
}
}

.marker {
cursor: pointer;
}

.hovered-row {
.backdrop {
fill-opacity: 100;
Expand Down
67 changes: 55 additions & 12 deletions projects/components/src/sequence/sequence-chart.component.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,32 @@ describe('Sequence Chart component', () => {
start: 1569357460346,
end: 1569357465346,
label: 'Segment 1',
color: 'blue'
color: 'blue',
markers: []
},
{
id: '2',
start: 1569357461346,
end: 1569357465346,
label: 'Segment 2',
color: 'green'
color: 'green',
markers: []
},
{
id: '3',
start: 1569357462346,
end: 1569357465346,
label: 'Segment 3',
color: 'green'
color: 'green',
markers: []
},
{
id: '4',
start: 1569357463346,
end: 1569357465346,
label: 'Segment 4',
color: 'green'
color: 'green',
markers: []
}
];
const chart = createHost(`<ht-sequence-chart [data]="data"></ht-sequence-chart>`, {
Expand Down Expand Up @@ -91,7 +95,8 @@ describe('Sequence Chart component', () => {
start: 1569357460346,
end: 1569357465346,
label: 'Segment 1',
color: 'blue'
color: 'blue',
markers: []
}
];
const chart = createHost(`<ht-sequence-chart [data]="data" [unit]="unit"></ht-sequence-chart>`, {
Expand All @@ -117,7 +122,8 @@ describe('Sequence Chart component', () => {
start: 1569357460346,
end: 1569357465346,
label: 'Segment 1',
color: 'blue'
color: 'blue',
markers: []
}
];
const chart = createHost(`<ht-sequence-chart [data]="data" [rowHeight]="rowHeight"></ht-sequence-chart>`, {
Expand All @@ -132,14 +138,46 @@ describe('Sequence Chart component', () => {
expect(dataRow!.getAttribute('height')).toEqual('50');
});

test('should render marker with correct width and height', () => {
const segmentsData = [
{
id: '1',
start: 1569357460346,
end: 1569357465346,
label: 'Segment 1',
color: 'blue',
markers: [
{
id: '1',
markerTime: 1569357460347,
timestamps: ['1569357460347']
}
]
}
];
const chart = createHost(`<ht-sequence-chart [data]="data"></ht-sequence-chart>`, {
hostProps: {
data: segmentsData,
rowHeight: 50
}
});

const markers = chart.queryAll('.marker', { root: true });
expect(markers.length).toBe(1);
const markerRect = select(markers[0]);
expect(markerRect.attr('width')).toBe('2');
expect(markerRect.attr('height')).toBe('12');
});

test('should render with correct bar height', () => {
const segmentsData = [
{
id: '1',
start: 1569357460346,
end: 1569357465346,
label: 'Segment 1',
color: 'blue'
color: 'blue',
markers: []
}
];
const chart = createHost(`<ht-sequence-chart [data]="data" [barHeight]="barHeight"></ht-sequence-chart>`, {
Expand All @@ -164,7 +202,8 @@ describe('Sequence Chart component', () => {
start: 1569357460346,
end: 1569357465346,
label: 'Segment 1',
color: 'blue'
color: 'blue',
markers: []
}
];
const chart = createHost(`<ht-sequence-chart [data]="data" [headerHeight]="headerHeight"></ht-sequence-chart>`, {
Expand All @@ -186,14 +225,16 @@ describe('Sequence Chart component', () => {
start: 1569357460346,
end: 1569357465346,
label: 'Segment 1',
color: 'blue'
color: 'blue',
markers: []
},
{
id: '2',
start: 1569357460346,
end: 1569357465346,
label: 'Segment 2',
color: 'green'
color: 'green',
markers: []
}
];
const chart = createHost(`<ht-sequence-chart [data]="data" [hovered]="hovered"></ht-sequence-chart>`, {
Expand Down Expand Up @@ -230,14 +271,16 @@ describe('Sequence Chart component', () => {
start: 1569357460346,
end: 1569357465346,
label: 'Segment 1',
color: 'blue'
color: 'blue',
markers: []
},
{
id: '2',
start: 1569357460346,
end: 1569357465346,
label: 'Segment 2',
color: 'green'
color: 'green',
markers: []
}
];
const chart = createHost(`<ht-sequence-chart [data]="data" [selection]="selection"></ht-sequence-chart>`, {
Expand Down
17 changes: 14 additions & 3 deletions projects/components/src/sequence/sequence-chart.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from '@angular/core';
import { RecursivePartial, TypedSimpleChanges } from '@hypertrace/common';
import { minBy } from 'lodash-es';
import { SequenceOptions, SequenceSegment } from './sequence';
import { Marker, MarkerDatum, SequenceOptions, SequenceSegment } from './sequence';
import { SequenceChartService } from './sequence-chart.service';
import { SequenceObject } from './sequence-object';

Expand Down Expand Up @@ -52,6 +52,9 @@ export class SequenceChartComponent implements OnChanges {
SequenceSegment | undefined
>();

@Output()
public readonly markerHoveredChange: EventEmitter<MarkerDatum> = new EventEmitter<MarkerDatum>();

@ViewChild('chartContainer', { static: true })
private readonly chartContainer!: ElementRef;

Expand Down Expand Up @@ -100,7 +103,8 @@ export class SequenceChartComponent implements OnChanges {
barHeight: this.barHeight,
unit: this.unit,
onSegmentSelected: (segment?: SequenceSegment) => this.onSegmentSelected(segment),
onSegmentHovered: (segment?: SequenceSegment) => this.onSegmentHovered(segment)
onSegmentHovered: (segment?: SequenceSegment) => this.onSegmentHovered(segment),
onMarkerHovered: (datum?: MarkerDatum) => this.onMarkerHovered(datum)
};
}

Expand All @@ -115,7 +119,10 @@ export class SequenceChartComponent implements OnChanges {
id: segment.id,
start: segment.start - minStart,
end: segment.end - minStart,
color: segment.color
color: segment.color,
markers: segment.markers
.map((marker: Marker) => ({ ...marker, markerTime: marker.markerTime - minStart }))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: some comments would be helpful

.sort((marker1, marker2) => marker1.markerTime - marker2.markerTime)
}));
}

Expand All @@ -128,4 +135,8 @@ export class SequenceChartComponent implements OnChanges {
this.hovered = segment;
this.hoveredChange.emit(segment);
}

private onMarkerHovered(datum?: MarkerDatum): void {
this.markerHoveredChange.emit(datum);
}
}
3 changes: 3 additions & 0 deletions projects/components/src/sequence/sequence-chart.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ export class SequenceChartService {
},
onSegmentHovered: () => {
/** NOOP */
},
onMarkerHovered: () => {
Comment thread
itssharmasandeep marked this conversation as resolved.
/** NOOP */
}
};
}
Expand Down
14 changes: 14 additions & 0 deletions projects/components/src/sequence/sequence.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ElementRef } from '@angular/core';
import { Selection } from 'd3-selection';
import { SequenceObject } from './sequence-object';

Expand All @@ -6,6 +7,18 @@ export interface SequenceSegment {
start: number;
end: number;
color: string;
markers: Marker[];
}

export interface Marker {
id: string;
markerTime: number;
timestamps: string[];
}

export interface MarkerDatum {
marker: Marker;
origin: ElementRef;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we could use the approach we discussed earlier then that would be great. Mainly, a way to avoid passing an elementRef

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will take a look in this.

}

/* Internal Types */
Expand All @@ -20,6 +33,7 @@ export interface SequenceOptions {
unit: string | undefined;
onSegmentSelected(row?: SequenceSegment): void;
onSegmentHovered(row?: SequenceSegment): void;
onMarkerHovered(datum?: MarkerDatum): void;
}

export interface MarginOptions {
Expand Down
Loading