|
|
|
import * as d3 from 'd3';
|
|
|
|
import * as _ from 'lodash';
|
|
|
|
|
|
|
|
|
|
|
|
const DEFAULT_ANNOTATION_OFFSET_Y = 4; //offset between triangle and bar in px
|
|
|
|
const DEFAULT_ANNOTATION_COLOR = 'red';
|
|
|
|
|
|
|
|
export class BarAnnotation {
|
|
|
|
position: { x: number, y: number };
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
protected overlay: d3.Selection<SVGRectElement, unknown, null, undefined>, // overlay from core. It should be global
|
|
|
|
protected groupContainer: d3.Selection<SVGGElement, unknown, null, undefined>, // group - bars as one item
|
|
|
|
protected annotationOptions: { size: number, max?: number, min?: number, offset?: number, color?: string },
|
|
|
|
) {
|
|
|
|
this.position = this.getGroupLastRectPosition();
|
|
|
|
this.renderAnnotation();
|
|
|
|
}
|
|
|
|
|
|
|
|
protected renderAnnotation(): void {
|
|
|
|
this.groupContainer.append('path')
|
|
|
|
.attr('d', () => this.getTrianglePath())
|
|
|
|
.attr('fill', this.annotationOptions.color || DEFAULT_ANNOTATION_COLOR)
|
|
|
|
.on('mouseover', this.redirectEventToOverlay.bind(this))
|
|
|
|
.on('mousemove', this.redirectEventToOverlay.bind(this))
|
|
|
|
.on('mouseout', this.redirectEventToOverlay.bind(this))
|
|
|
|
.on('mousedown', () => { d3.event.stopPropagation(); });
|
|
|
|
}
|
|
|
|
|
|
|
|
getGroupLastRectPosition(): { x: number, y: number } {
|
|
|
|
const lastRect = _.last(this.groupContainer.selectAll('rect')?.nodes());
|
|
|
|
const barSelection = d3.select(lastRect);
|
|
|
|
return {
|
|
|
|
x: Math.ceil(_.toNumber(barSelection.attr('x'))),
|
|
|
|
y: Math.ceil(_.toNumber(barSelection.attr('y'))),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected getTrianglePath(): string {
|
|
|
|
// (x, y) - top left corner of bar
|
|
|
|
const minTriangleSize = this.annotationOptions?.min;
|
|
|
|
const maxTriagleSize = this.annotationOptions?.max;
|
|
|
|
const yOffset = this.annotationOptions?.offset || DEFAULT_ANNOTATION_OFFSET_Y;
|
|
|
|
const centerX = this.position.x + this.annotationOptions.size / 2;
|
|
|
|
const correctedLength = _.clamp(this.annotationOptions.size, minTriangleSize, maxTriagleSize);
|
|
|
|
|
|
|
|
const topY = Math.max(this.position.y - correctedLength - yOffset, 4);
|
|
|
|
const topLeftCorner = {
|
|
|
|
x: centerX - correctedLength / 2,
|
|
|
|
y: topY,
|
|
|
|
};
|
|
|
|
const topRightCorner = {
|
|
|
|
x: centerX + correctedLength / 2,
|
|
|
|
y: topY,
|
|
|
|
};
|
|
|
|
const bottomMiddleCorner = {
|
|
|
|
x: centerX,
|
|
|
|
y: topY + correctedLength,
|
|
|
|
};
|
|
|
|
|
|
|
|
return `M ${topLeftCorner.x} ${topLeftCorner.y}
|
|
|
|
L ${topRightCorner.x} ${topRightCorner.y}
|
|
|
|
L ${bottomMiddleCorner.x} ${bottomMiddleCorner.y} z`;
|
|
|
|
}
|
|
|
|
|
|
|
|
redirectEventToOverlay(): void {
|
|
|
|
this.overlay?.node().dispatchEvent(new MouseEvent(d3.event.type, d3.event));
|
|
|
|
}
|
|
|
|
}
|