projects/swimlane/ngx-charts/src/lib/box-chart/box-chart.component.ts
import {
ChangeDetectionStrategy,
Component,
ContentChild,
EventEmitter,
Input,
Output,
TemplateRef,
ViewEncapsulation
} from '@angular/core';
import { BaseChartComponent } from '../common/base-chart.component';
import { ColorHelper } from '../common/color.helper';
import { BoxChartMultiSeries, BoxChartSeries, IBoxModel, StringOrNumberOrDate } from '../models/chart-data.model';
import { scaleLinear, ScaleLinear, scaleBand, ScaleBand } from 'd3-scale';
import { calculateViewDimensions } from '../common/view-dimensions.helper';
import { ViewDimensions } from '../common/types/view-dimension.interface';
import { LegendPosition, LegendOptions } from '../common/types/legend.model';
import { ScaleType } from '../common/types/scale-type.enum';
@Component({
selector: 'ngx-charts-box-chart',
template: `
<ngx-charts-chart
[view]="[width, height]"
[showLegend]="legend"
[legendOptions]="legendOptions"
[animations]="animations"
(legendLabelClick)="onClick($event)"
(legendLabelActivate)="onActivate($event)"
(legendLabelDeactivate)="onDeactivate($event)"
>
<svg:g [attr.transform]="transform" class="box-chart chart">
<svg:g
ngx-charts-x-axis
[showGridLines]="showGridLines"
[dims]="dims"
[xScale]="xScale"
[showLabel]="showXAxisLabel"
[labelText]="xAxisLabel"
[wrapTicks]="wrapTicks"
(dimensionsChanged)="updateXAxisHeight($event)"
/>
<svg:g
ngx-charts-y-axis
[showGridLines]="showGridLines"
[dims]="dims"
[yScale]="yScale"
[showLabel]="showYAxisLabel"
[labelText]="yAxisLabel"
[wrapTicks]="wrapTicks"
(dimensionsChanged)="updateYAxisWidth($event)"
/>
</svg:g>
<svg:g [attr.transform]="transform">
<svg:g *ngFor="let result of results; trackBy: trackBy">
<svg:g
ngx-charts-box-series
[xScale]="xScale"
[yScale]="yScale"
[colors]="colors"
[roundEdges]="roundEdges"
[strokeColor]="strokeColor"
[strokeWidth]="strokeWidth"
[tooltipDisabled]="tooltipDisabled"
[tooltipTemplate]="tooltipTemplate"
[series]="result"
[dims]="dims"
[animations]="animations"
[gradient]="gradient"
(activate)="onActivate($event)"
(deactivate)="onDeactivate($event)"
(select)="onClick($event)"
/>
</svg:g>
</svg:g>
</ngx-charts-chart>
`,
styleUrls: ['../common/base-chart.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None
})
export class BoxChartComponent extends BaseChartComponent {
/** Show or hide the legend. */
@Input() legend: boolean = false;
@Input() legendPosition: LegendPosition = LegendPosition.Right;
@Input() legendTitle: string = 'Legend';
/** I think it is better to handle legend options as single Input object of type ILegendOptions */
@Input() legendOptionsConfig: LegendOptions;
@Input() showGridLines: boolean = true;
@Input() xAxis: boolean = true;
@Input() yAxis: boolean = true;
@Input() showXAxisLabel: boolean = true;
@Input() showYAxisLabel: boolean = true;
@Input() roundDomains: boolean = false;
@Input() xAxisLabel: string;
@Input() yAxisLabel: string;
@Input() roundEdges: boolean = true;
@Input() strokeColor: string = '#FFFFFF';
@Input() strokeWidth: number = 2;
@Input() tooltipDisabled: boolean = false;
@Input() gradient: boolean;
@Input() wrapTicks = false;
@Output() select: EventEmitter<IBoxModel> = new EventEmitter();
@Output() activate: EventEmitter<IBoxModel> = new EventEmitter();
@Output() deactivate: EventEmitter<IBoxModel> = new EventEmitter();
@ContentChild('tooltipTemplate', { static: false }) tooltipTemplate: TemplateRef<any>;
/** Input Data, this came from Base Chart Component. */
results: BoxChartMultiSeries;
/** Chart Dimensions, this came from Base Chart Component. */
dims: ViewDimensions;
/** Color data. */
colors: ColorHelper;
/** Transform string css attribute for the chart container */
transform: string;
/** Chart Margins (For each side, counterclock wise). */
margin: [number, number, number, number] = [10, 20, 10, 20];
/** Legend Options object to handle positioning, title, colors and domain. */
legendOptions: LegendOptions;
xScale: ScaleBand<string>;
yScale: ScaleLinear<number, number>;
xDomain: StringOrNumberOrDate[];
yDomain: number[];
seriesDomain: string[];
/** Chart X axis dimension. */
xAxisHeight: number = 0;
/** Chart Y axis dimension. */
yAxisWidth: number = 0;
trackBy(index: number, item: BoxChartSeries): StringOrNumberOrDate {
return item.name;
}
update(): void {
super.update();
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,
legendPosition: this.legendPosition
});
this.xDomain = this.getXDomain();
this.yDomain = this.getYDomain();
this.seriesDomain = this.getSeriesDomain();
this.setScales();
this.setColors();
this.legendOptions = this.getLegendOptions();
this.transform = `translate(${this.dims.xOffset} , ${this.margin[0]})`;
}
setColors(): void {
let domain: string[] | number[] = [];
if (this.schemeType === ScaleType.Ordinal) {
domain = this.seriesDomain;
} else {
domain = this.yDomain;
}
this.colors = new ColorHelper(this.scheme, this.schemeType, domain, this.customColors);
}
setScales() {
this.xScale = this.getXScale(this.xDomain, this.dims.width);
this.yScale = this.getYScale(this.yDomain, this.dims.height);
}
getXScale(domain: Array<string | number | Date>, width: number): ScaleBand<string> {
const scale = scaleBand()
.domain(domain.map(d => d.toString()))
.rangeRound([0, width])
.padding(0.5);
return scale;
}
getYScale(domain: number[], height: number): ScaleLinear<number, number> {
const scale = scaleLinear().domain(domain).range([height, 0]);
return this.roundDomains ? scale.nice() : scale;
}
getUniqueBoxChartXDomainValues(results: BoxChartMultiSeries) {
const valueSet = new Set<string | number | Date>();
for (const result of results) {
valueSet.add(result.name);
}
return Array.from(valueSet);
}
getXDomain(): Array<string | number | Date> {
let domain: Array<string | number | Date> = [];
const values: Array<string | number | Date> = this.getUniqueBoxChartXDomainValues(this.results);
let min: number;
let max: number;
if (typeof values[0] === 'string') {
domain = values.map(val => val.toString());
} else if (typeof values[0] === 'number') {
const mappedValues = values.map(v => Number(v));
min = Math.min(...mappedValues);
max = Math.max(...mappedValues);
domain = [min, max];
} else {
const mappedValues = values.map(v => Number(new Date(v)));
min = Math.min(...mappedValues);
max = Math.max(...mappedValues);
domain = [new Date(min), new Date(max)];
}
return domain;
}
getYDomain(): number[] {
const domain: Array<number | Date> = [];
for (const results of this.results) {
for (const d of results.series) {
if (domain.indexOf(d.value) < 0) {
domain.push(d.value);
}
}
}
const values = [...domain];
const mappedValues = values.map(v => Number(v));
const min: number = Math.min(...mappedValues);
const max: number = Math.max(...mappedValues);
return [min, max];
}
getSeriesDomain(): string[] {
return this.results.map(d => `${d.name}`);
}
updateYAxisWidth({ width }): void {
this.yAxisWidth = width;
this.update();
}
updateXAxisHeight({ height }): void {
this.xAxisHeight = height;
this.update();
}
onClick(data: IBoxModel): void {
this.select.emit(data);
}
onActivate(data: IBoxModel): void {
this.activate.emit(data);
}
onDeactivate(data: IBoxModel): void {
this.deactivate.emit(data);
}
private getLegendOptions(): LegendOptions {
const legendOpts: LegendOptions = {
scaleType: this.schemeType,
colors: this.colors,
domain: [],
position: this.legendPosition,
title: this.legendTitle
};
if (this.schemeType === ScaleType.Ordinal) {
legendOpts.domain = this.xDomain;
legendOpts.colors = this.colors;
} else {
legendOpts.domain = this.yDomain;
legendOpts.colors = this.colors.scale;
}
return legendOpts;
}
}