projects/swimlane/ngx-charts/src/lib/common/tooltip-area.component.ts
import {
Component,
Input,
Output,
EventEmitter,
ViewChild,
ChangeDetectionStrategy,
TemplateRef,
PLATFORM_ID,
Inject
} from '@angular/core';
import { trigger, style, animate, transition } from '@angular/animations';
import { createMouseEvent } from '../events';
import { isPlatformBrowser } from '@angular/common';
import { ColorHelper } from '../common/color.helper';
import { PlacementTypes } from './tooltip/position';
import { StyleTypes } from './tooltip/style.type';
import { ViewDimensions } from './types/view-dimension.interface';
import { ScaleType } from './types/scale-type.enum';
export interface Tooltip {
color: string;
d0: number;
d1: number;
max: number;
min: number;
name: any;
series: any;
value: any;
}
@Component({
selector: 'g[ngx-charts-tooltip-area]',
template: `
<svg:g>
<svg:rect
class="tooltip-area"
[attr.x]="0"
y="0"
[attr.width]="dims.width"
[attr.height]="dims.height"
style="opacity: 0; cursor: 'auto';"
(mousemove)="mouseMove($event)"
(mouseleave)="hideTooltip()"
/>
<ng-template #defaultTooltipTemplate let-model="model">
<xhtml:div class="area-tooltip-container">
<xhtml:div *ngFor="let tooltipItem of model" class="tooltip-item">
<xhtml:span class="tooltip-item-color" [style.background-color]="tooltipItem.color"></xhtml:span>
{{ getToolTipText(tooltipItem) }}
</xhtml:div>
</xhtml:div>
</ng-template>
<svg:rect
#tooltipAnchor
[@animationState]="anchorOpacity !== 0 ? 'active' : 'inactive'"
class="tooltip-anchor"
[attr.x]="anchorPos"
y="0"
[attr.width]="1"
[attr.height]="dims.height"
[style.opacity]="anchorOpacity"
[style.pointer-events]="'none'"
ngx-tooltip
[tooltipDisabled]="tooltipDisabled"
[tooltipPlacement]="placementTypes.Right"
[tooltipType]="styleTypes.tooltip"
[tooltipSpacing]="15"
[tooltipTemplate]="tooltipTemplate ? tooltipTemplate : defaultTooltipTemplate"
[tooltipContext]="anchorValues"
[tooltipImmediateExit]="true"
/>
</svg:g>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [
trigger('animationState', [
transition('inactive => active', [
style({
opacity: 0
}),
animate(250, style({ opacity: 0.7 }))
]),
transition('active => inactive', [
style({
opacity: 0.7
}),
animate(250, style({ opacity: 0 }))
])
])
]
})
export class TooltipArea {
anchorOpacity: number = 0;
anchorPos: number = -1;
anchorValues: Tooltip[] = [];
lastAnchorPos: number;
placementTypes = PlacementTypes;
styleTypes = StyleTypes;
@Input() dims: ViewDimensions;
@Input() xSet: any[];
@Input() xScale;
@Input() yScale;
@Input() results: any[];
@Input() colors: ColorHelper;
@Input() showPercentage: boolean = false;
@Input() tooltipDisabled: boolean = false;
@Input() tooltipTemplate: TemplateRef<any>;
@Output() hover: EventEmitter<{ value: any }> = new EventEmitter();
@ViewChild('tooltipAnchor', { static: false }) tooltipAnchor;
constructor(@Inject(PLATFORM_ID) private platformId: any) {}
getValues(xVal): Tooltip[] {
const results = [];
for (const group of this.results) {
const item = group.series.find(d => d.name.toString() === xVal.toString());
let groupName = group.name;
if (groupName instanceof Date) {
groupName = groupName.toLocaleDateString();
}
if (item) {
const label = item.name;
let val = item.value;
if (this.showPercentage) {
val = (item.d1 - item.d0).toFixed(2) + '%';
}
let color;
if (this.colors.scaleType === ScaleType.Linear) {
let v = val;
if (item.d1) {
v = item.d1;
}
color = this.colors.getColor(v);
} else {
color = this.colors.getColor(group.name);
}
const data = Object.assign({}, item, {
value: val,
name: label,
series: groupName,
min: item.min,
max: item.max,
color
});
results.push(data);
}
}
return results;
}
mouseMove(event) {
if (!isPlatformBrowser(this.platformId)) {
return;
}
const xPos = event.pageX - event.target.getBoundingClientRect().left;
const closestIndex = this.findClosestPointIndex(xPos);
const closestPoint = this.xSet[closestIndex];
this.anchorPos = this.xScale(closestPoint);
this.anchorPos = Math.max(0, this.anchorPos);
this.anchorPos = Math.min(this.dims.width, this.anchorPos);
this.anchorValues = this.getValues(closestPoint);
if (this.anchorPos !== this.lastAnchorPos) {
const ev = createMouseEvent('mouseleave');
this.tooltipAnchor.nativeElement.dispatchEvent(ev);
this.anchorOpacity = 0.7;
this.hover.emit({
value: closestPoint
});
this.showTooltip();
this.lastAnchorPos = this.anchorPos;
}
}
findClosestPointIndex(xPos: number): number {
let minIndex = 0;
let maxIndex = this.xSet.length - 1;
let minDiff = Number.MAX_VALUE;
let closestIndex = 0;
while (minIndex <= maxIndex) {
const currentIndex = ((minIndex + maxIndex) / 2) | 0;
const currentElement = this.xScale(this.xSet[currentIndex]);
const curDiff = Math.abs(currentElement - xPos);
if (curDiff < minDiff) {
minDiff = curDiff;
closestIndex = currentIndex;
}
if (currentElement < xPos) {
minIndex = currentIndex + 1;
} else if (currentElement > xPos) {
maxIndex = currentIndex - 1;
} else {
minDiff = 0;
closestIndex = currentIndex;
break;
}
}
return closestIndex;
}
showTooltip(): void {
const event = createMouseEvent('mouseenter');
this.tooltipAnchor.nativeElement.dispatchEvent(event);
}
hideTooltip(): void {
const event = createMouseEvent('mouseleave');
this.tooltipAnchor.nativeElement.dispatchEvent(event);
this.anchorOpacity = 0;
this.lastAnchorPos = -1;
}
getToolTipText(tooltipItem: Tooltip): string {
let result: string = '';
if (tooltipItem.series !== undefined) {
result += tooltipItem.series;
} else {
result += '???';
}
result += ': ';
if (tooltipItem.value !== undefined) {
result += tooltipItem.value.toLocaleString();
}
if (tooltipItem.min !== undefined || tooltipItem.max !== undefined) {
result += ' (';
if (tooltipItem.min !== undefined) {
if (tooltipItem.max === undefined) {
result += '≥';
}
result += tooltipItem.min.toLocaleString();
if (tooltipItem.max !== undefined) {
result += ' - ';
}
} else if (tooltipItem.max !== undefined) {
result += '≤';
}
if (tooltipItem.max !== undefined) {
result += tooltipItem.max.toLocaleString();
}
result += ')';
}
return result;
}
}