src/heat-map/heat-map.component.ts
import {
Component,
Input,
ViewEncapsulation,
ChangeDetectionStrategy,
ContentChild,
TemplateRef,
Output,
EventEmitter
} from '@angular/core';
import { scaleBand } from 'd3-scale';
import { BaseChartComponent } from '../common/base-chart.component';
import { calculateViewDimensions, ViewDimensions } from '../common/view-dimensions.helper';
import { ColorHelper } from '../common/color.helper';
import { getScaleType } from '../common/domain.helper';
import { ScaleType } from '../utils/scale-type.enum';
import { LegendOptions } from '../common/legend/legend-options';
import { LegendPosition } from '../common/legend/legend-position.enum';
@Component({
selector: 'ngx-charts-heat-map',
template: `
<ngx-charts-chart
[view]="[width, height]"
[showLegend]="legend"
[animations]="animations"
[legendOptions]="legendOptions"
(legendLabelClick)="onClick($event)"
>
<svg:g [attr.transform]="transform" class="heat-map chart">
<svg:g
ngx-charts-x-axis
*ngIf="xAxis"
[xScale]="xScale"
[dims]="dims"
[showLabel]="showXAxisLabel"
[labelText]="xAxisLabel"
[trimTicks]="trimXAxisTicks"
[rotateTicks]="rotateXAxisTicks"
[maxTickLength]="maxXAxisTickLength"
[tickFormatting]="xAxisTickFormatting"
[ticks]="xAxisTicks"
(dimensionsChanged)="updateXAxisHeight($event)"
></svg:g>
<svg:g
ngx-charts-y-axis
*ngIf="yAxis"
[yScale]="yScale"
[dims]="dims"
[showLabel]="showYAxisLabel"
[labelText]="yAxisLabel"
[trimTicks]="trimYAxisTicks"
[maxTickLength]="maxYAxisTickLength"
[tickFormatting]="yAxisTickFormatting"
[ticks]="yAxisTicks"
(dimensionsChanged)="updateYAxisWidth($event)"
></svg:g>
<svg:rect
*ngFor="let rect of rects"
[attr.x]="rect.x"
[attr.y]="rect.y"
[attr.rx]="rect.rx"
[attr.width]="rect.width"
[attr.height]="rect.height"
[attr.fill]="rect.fill"
/>
<svg:g
ngx-charts-heat-map-cell-series
[xScale]="xScale"
[yScale]="yScale"
[colors]="colors"
[data]="results"
[gradient]="gradient"
[animations]="animations"
[tooltipDisabled]="tooltipDisabled"
[tooltipTemplate]="tooltipTemplate"
[tooltipText]="tooltipText"
(select)="onClick($event)"
(activate)="onActivate($event, undefined)"
(deactivate)="onDeactivate($event, undefined)"
/>
</svg:g>
</ngx-charts-chart>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrls: ['../common/base-chart.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class HeatMapComponent extends BaseChartComponent {
@Input() legend;
@Input() legendTitle: string = 'Legend';
@Input() legendPosition = LegendPosition.right;
@Input() xAxis;
@Input() yAxis;
@Input() showXAxisLabel;
@Input() showYAxisLabel;
@Input() xAxisLabel;
@Input() yAxisLabel;
@Input() gradient: boolean;
@Input() innerPadding: number | number[] = 8;
@Input() trimXAxisTicks: boolean = true;
@Input() trimYAxisTicks: boolean = true;
@Input() rotateXAxisTicks: boolean = true;
@Input() maxXAxisTickLength: number = 16;
@Input() maxYAxisTickLength: number = 16;
@Input() xAxisTickFormatting: any;
@Input() yAxisTickFormatting: any;
@Input() xAxisTicks: any[];
@Input() yAxisTicks: any[];
@Input() tooltipDisabled: boolean = false;
@Input() tooltipText: any;
@Input() min: any;
@Input() max: any;
@Input() activeEntries: any[] = [];
@Output() activate: EventEmitter<any> = new EventEmitter();
@Output() deactivate: EventEmitter<any> = new EventEmitter();
@ContentChild('tooltipTemplate', { static: false }) tooltipTemplate: TemplateRef<any>;
dims: ViewDimensions;
xDomain: any[];
yDomain: any[];
valueDomain: any[];
xScale: any;
yScale: any;
color: any;
colors: ColorHelper;
colorScale: any;
transform: string;
rects: any[];
margin = [10, 20, 10, 20];
xAxisHeight: number = 0;
yAxisWidth: number = 0;
legendOptions: any;
scaleType = ScaleType.linear;
update(): void {
super.update();
this.formatDates();
this.xDomain = this.getXDomain();
this.yDomain = this.getYDomain();
this.valueDomain = this.getValueDomain();
this.scaleType = getScaleType(this.valueDomain, false);
this.dims = calculateViewDimensions({
width: this.width,
height: this.height,
margins: this.margin,
showXAxis: this.xAxis,
showYAxis: this.yAxis,
xAxisHeight: this.xAxisHeight,
yAxisWidth: this.yAxisWidth,
showXLabel: this.showXAxisLabel,
showYLabel: this.showYAxisLabel,
showLegend: this.legend,
legendType: this.scaleType,
legendPosition: this.legendPosition
});
if (this.scaleType === 'linear') {
let min = this.min;
let max = this.max;
if (!this.min) {
min = Math.min(0, ...this.valueDomain);
}
if (!this.max) {
max = Math.max(...this.valueDomain);
}
this.valueDomain = [min, max];
}
this.xScale = this.getXScale();
this.yScale = this.getYScale();
this.setColors();
this.legendOptions = this.getLegendOptions();
this.transform = `translate(${this.dims.xOffset} , ${this.margin[0]})`;
this.rects = this.getRects();
}
getXDomain(): any {
const domain = [];
for (const group of this.results) {
if (!domain.includes(group.name)) {
domain.push(group.name);
}
}
return domain;
}
getYDomain(): any[] {
const domain = [];
for (const group of this.results) {
for (const d of group.series) {
if (!domain.includes(d.name)) {
domain.push(d.name);
}
}
}
return domain;
}
getValueDomain(): any[] {
const domain = [];
for (const group of this.results) {
for (const d of group.series) {
if (!domain.includes(d.value)) {
domain.push(d.value);
}
}
}
return domain;
}
/**
* Converts the input to gap paddingInner in fraction
* Supports the following inputs:
* Numbers: 8
* Strings: "8", "8px", "8%"
* Arrays: [8,2], "8,2", "[8,2]"
* Mixed: [8,"2%"], ["8px","2%"], "8,2%", "[8,2%]"
*
* @param {(string | number | Array<string | number>)} value
* @param {number} [index=0]
* @param {number} N
* @param {number} L
* @returns {number}
*
* @memberOf HeatMapComponent
*/
getDimension(value: string | number | Array<string | number>, index = 0, N: number, L: number): number {
if (typeof value === 'string') {
value = value
.replace('[', '')
.replace(']', '')
.replace('px', '')
.replace('\'', '');
if (value.includes(',')) {
value = value.split(',');
}
}
if (Array.isArray(value) && typeof index === 'number') {
return this.getDimension(value[index], null, N, L);
}
if (typeof value === 'string' && value.includes('%')) {
return +value.replace('%', '') / 100;
}
return N / (L / +value + 1);
}
getXScale(): any {
const f = this.getDimension(this.innerPadding, 0, this.xDomain.length, this.dims.width);
return scaleBand()
.rangeRound([0, this.dims.width])
.domain(this.xDomain)
.paddingInner(f);
}
getYScale(): any {
const f = this.getDimension(this.innerPadding, 1, this.yDomain.length, this.dims.height);
return scaleBand()
.rangeRound([this.dims.height, 0])
.domain(this.yDomain)
.paddingInner(f);
}
getRects(): any[] {
const rects = [];
this.xDomain.map(xVal => {
this.yDomain.map(yVal => {
rects.push({
x: this.xScale(xVal),
y: this.yScale(yVal),
rx: 3,
width: this.xScale.bandwidth(),
height: this.yScale.bandwidth(),
fill: 'rgba(200,200,200,0.03)'
});
});
});
return rects;
}
onClick(data): void {
this.select.emit(data);
}
setColors(): void {
this.colors = new ColorHelper(this.scheme, this.scaleType, this.valueDomain);
}
getLegendOptions(): LegendOptions {
return {
scaleType: this.scaleType,
domain: this.valueDomain,
colors: this.scaleType === 'ordinal' ? this.colors : this.colors.scale,
title: this.scaleType === 'ordinal' ? this.legendTitle : undefined,
position: this.legendPosition
};
}
updateYAxisWidth({ width }): void {
this.yAxisWidth = width;
this.update();
}
updateXAxisHeight({ height }): void {
this.xAxisHeight = height;
this.update();
}
onActivate(event, group, fromLegend = false) {
const item = Object.assign({}, event);
if (group) {
item.series = group.name;
}
const items = this.results
.map(g => g.series)
.flat()
.filter(i => {
if (fromLegend) {
return i.label === item.name;
} else {
return i.name === item.name && i.series === item.series;
}
});
this.activeEntries = [...items];
this.activate.emit({ value: item, entries: this.activeEntries });
}
onDeactivate(event, group, fromLegend = false) {
const item = Object.assign({}, event);
if (group) {
item.series = group.name;
}
this.activeEntries = this.activeEntries.filter(i => {
if (fromLegend) {
return i.label !== item.name;
} else {
return !(i.name === item.name && i.series === item.series);
}
});
this.deactivate.emit({ value: item, entries: this.activeEntries });
}
}