Skip to content

Instantly share code, notes, and snippets.

@gilmarsquinelato
Created September 28, 2018 13:52
Show Gist options
  • Save gilmarsquinelato/c18000ec3344bb6c4c8465550fed80f8 to your computer and use it in GitHub Desktop.
Save gilmarsquinelato/c18000ec3344bb6c4c8465550fed80f8 to your computer and use it in GitHub Desktop.
ngx-charts realtime line chart component possible solution
import {
Component,
Input,
Output,
EventEmitter,
ViewEncapsulation,
HostListener,
ChangeDetectionStrategy,
ContentChild,
TemplateRef,
} from '@angular/core';
import {
trigger,
style,
animate,
transition
} from '@angular/animations';
import { scaleLinear, scaleTime, scalePoint } from 'd3-scale';
import { curveLinear } from 'd3-shape';
import { LineChartComponent as BaseLineChartComponent} from '@swimlane/ngx-charts/release/line-chart/line-chart.component';
import { calculateViewDimensions, ViewDimensions } from '@swimlane/ngx-charts/release/common/view-dimensions.helper';
import { ColorHelper } from '@swimlane/ngx-charts/release/common/color.helper';
import { id } from '@swimlane/ngx-charts/release/utils/id';
import { getUniqueXDomainValues } from '@swimlane/ngx-charts/release/common/domain.helper';
@Component({
selector: 'app-line-chart',
template: `
<ngx-charts-chart
[view]="[width, height]"
[showLegend]="legend"
[legendOptions]="legendOptions"
[activeEntries]="activeEntries"
[animations]="animations"
(legendLabelClick)="onClick($event)"
(legendLabelActivate)="onActivate($event)"
(legendLabelDeactivate)="onDeactivate($event)">
<svg:defs>
<svg:clipPath [attr.id]="clipPathId">
<svg:rect
[attr.width]="dims.width + 10"
[attr.height]="dims.height + 10"
[attr.transform]="'translate(-5, -5)'"/>
</svg:clipPath>
</svg:defs>
<svg:g [attr.transform]="transform" class="line-chart chart">
<svg:g ngx-charts-x-axis
*ngIf="xAxis"
[xScale]="xScale"
[dims]="dims"
[showGridLines]="showGridLines"
[showLabel]="showXAxisLabel"
[labelText]="xAxisLabel"
[tickFormatting]="xAxisTickFormatting"
[ticks]="xAxisTicks"
(dimensionsChanged)="updateXAxisHeight($event)">
</svg:g>
<svg:g ngx-charts-y-axis
*ngIf="yAxis"
[yScale]="yScale"
[dims]="dims"
[showGridLines]="showGridLines"
[showLabel]="showYAxisLabel"
[labelText]="yAxisLabel"
[tickFormatting]="yAxisTickFormatting"
[ticks]="yAxisTicks"
[referenceLines]="referenceLines"
[showRefLines]="showRefLines"
[showRefLabels]="showRefLabels"
(dimensionsChanged)="updateYAxisWidth($event)">
</svg:g>
<svg:g [attr.clip-path]="clipPath">
<svg:g *ngFor="let series of localResults; trackBy:trackBy" [@animationState]="'active'">
<svg:g ngx-charts-line-series
[xScale]="xScale"
[yScale]="yScale"
[colors]="colors"
[data]="series"
[activeEntries]="activeEntries"
[scaleType]="scaleType"
[curve]="curve"
[rangeFillOpacity]="rangeFillOpacity"
[hasRange]="hasRange"
[animations]="animations"
/>
</svg:g>
<svg:g *ngIf="!tooltipDisabled" (mouseleave)="hideCircles()">
<svg:g ngx-charts-tooltip-area
[dims]="dims"
[xSet]="xSet"
[xScale]="xScale"
[yScale]="yScale"
[results]="results"
[colors]="colors"
[tooltipDisabled]="tooltipDisabled"
[tooltipTemplate]="seriesTooltipTemplate"
(hover)="updateHoveredVertical($event)"
/>
<svg:g *ngFor="let series of localResults">
<svg:g ngx-charts-circle-series
[xScale]="xScale"
[yScale]="yScale"
[colors]="colors"
[data]="series"
[scaleType]="scaleType"
[visibleValue]="hoveredVertical"
[activeEntries]="activeEntries"
[tooltipDisabled]="tooltipDisabled"
[tooltipTemplate]="tooltipTemplate"
(select)="onClick($event, series)"
(activate)="onActivate($event)"
(deactivate)="onDeactivate($event)"
/>
</svg:g>
</svg:g>
</svg:g>
</svg:g>
<svg:g ngx-charts-timeline
*ngIf="timeline && scaleType != 'ordinal'"
[attr.transform]="timelineTransform"
[results]="results"
[view]="[timelineWidth, height]"
[height]="timelineHeight"
[scheme]="scheme"
[customColors]="customColors"
[scaleType]="scaleType"
[legend]="legend"
(onDomainChange)="updateDomain($event)">
<svg:g *ngFor="let series of localResults; trackBy:trackBy">
<svg:g ngx-charts-line-series
[xScale]="timelineXScale"
[yScale]="timelineYScale"
[colors]="colors"
[data]="series"
[scaleType]="scaleType"
[curve]="curve"
[hasRange]="hasRange"
[animations]="animations"
/>
</svg:g>
</svg:g>
</ngx-charts-chart>
`,
styleUrls: ['./line-chart.component.styl'],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [
trigger('animationState', [
transition(':leave', [
style({
opacity: 1,
}),
animate(500, style({
opacity: 0
}))
])
])
]
})
export class LineChartComponent extends BaseLineChartComponent {
@Input() append = false;
@Input() legend;
@Input() legendTitle = 'Legend';
@Input() xAxis;
@Input() yAxis;
@Input() showXAxisLabel;
@Input() showYAxisLabel;
@Input() xAxisLabel;
@Input() yAxisLabel;
@Input() autoScale;
@Input() timeline;
@Input() gradient: boolean;
@Input() showGridLines = true;
@Input() curve: any = curveLinear;
@Input() activeEntries: any[] = [];
@Input() schemeType: string;
@Input() rangeFillOpacity: number;
@Input() xAxisTickFormatting: any;
@Input() yAxisTickFormatting: any;
@Input() xAxisTicks: any[];
@Input() yAxisTicks: any[];
@Input() roundDomains = false;
@Input() tooltipDisabled = false;
@Input() showRefLines = false;
@Input() referenceLines: any;
@Input() showRefLabels = true;
@Input() xScaleMin: any;
@Input() xScaleMax: any;
@Input() yScaleMin: number;
@Input() yScaleMax: number;
@Output() activate: EventEmitter<any> = new EventEmitter();
@Output() deactivate: EventEmitter<any> = new EventEmitter();
@ContentChild('tooltipTemplate') tooltipTemplate: TemplateRef<any>;
@ContentChild('seriesTooltipTemplate') seriesTooltipTemplate: TemplateRef<any>;
localResults: any[];
dims: ViewDimensions;
xSet: any;
xDomain: any;
yDomain: any;
seriesDomain: any;
yScale: any;
xScale: any;
colors: ColorHelper;
scaleType: string;
transform: string;
clipPath: string;
clipPathId: string;
series: any;
areaPath: any;
margin = [10, 20, 10, 20];
hoveredVertical: any; // the value of the x axis that is hovered over
xAxisHeight = 0;
yAxisWidth = 0;
filteredDomain: any;
legendOptions: any;
hasRange: boolean; // whether the line has a min-max range around it
timelineWidth: any;
timelineHeight = 50;
timelineXScale: any;
timelineYScale: any;
timelineXDomain: any;
timelineTransform: any;
timelinePadding = 10;
update(): void {
super.update();
if (!this.localResults || !this.append) {
this.localResults = this.results;
} else {
this.appendNewData();
}
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.schemeType,
});
if (this.timeline) {
this.dims.height -= (this.timelineHeight + this.margin[2] + this.timelinePadding);
}
this.xDomain = this.getXDomain();
if (this.filteredDomain) {
this.xDomain = this.filteredDomain;
}
this.yDomain = this.getYDomain();
this.seriesDomain = this.getSeriesDomain();
this.xScale = this.getXScale(this.xDomain, this.dims.width);
this.yScale = this.getYScale(this.yDomain, this.dims.height);
this.updateTimeline();
this.setColors();
this.legendOptions = this.getLegendOptions();
this.transform = `translate(${ this.dims.xOffset } , ${ this.margin[0] })`;
this.clipPathId = 'clip' + id().toString();
this.clipPath = `url(#${this.clipPathId})`;
}
appendNewData(): void {
for (let i = 0; i < this.results.length; ++i) {
if (!this.localResults[i]) {
this.localResults.push(this.results[i]);
} else {
this.appendDataToSeries(i);
}
}
}
appendDataToSeries(index: number): void {
const lastSeries = this.localResults[index].series.slice(-1)[0];
if (lastSeries) {
const seriesIndex = this.results[index].series.map(i => i.name).indexOf(lastSeries.name);
this.localResults[index].series.push(...this.results[index].series.slice(seriesIndex + 1));
}
}
updateTimeline(): void {
if (this.timeline) {
this.timelineWidth = this.dims.width;
this.timelineXDomain = this.getXDomain();
this.timelineXScale = this.getXScale(this.timelineXDomain, this.timelineWidth);
this.timelineYScale = this.getYScale(this.yDomain, this.timelineHeight);
this.timelineTransform = `translate(${ this.dims.xOffset }, ${ -this.margin[2] })`;
}
}
getXDomain(): any[] {
let values = getUniqueXDomainValues(this.results);
this.scaleType = this.getScaleType(values);
let domain = [];
if (this.scaleType === 'linear') {
values = values.map(v => Number(v));
}
let min;
let max;
if (this.scaleType === 'time' || this.scaleType === 'linear') {
min = this.xScaleMin
? this.xScaleMin
: Math.min(...values);
max = this.xScaleMax
? this.xScaleMax
: Math.max(...values);
}
if (this.scaleType === 'time') {
domain = [new Date(min), new Date(max)];
this.xSet = [...values].sort((a, b) => {
const aDate = a.getTime();
const bDate = b.getTime();
if (aDate > bDate) { return 1; }
if (bDate > aDate) { return -1; }
return 0;
});
} else if (this.scaleType === 'linear') {
domain = [min, max];
// Use compare function to sort numbers numerically
this.xSet = [...values].sort((a, b) => (a - b));
} else {
domain = values;
this.xSet = values;
}
return domain;
}
getYDomain(): any[] {
const domain = [];
for (const results of this.results) {
for (const d of results.series) {
if (domain.indexOf(d.value) < 0) {
domain.push(d.value);
}
if (d.min !== undefined) {
this.hasRange = true;
if (domain.indexOf(d.min) < 0) {
domain.push(d.min);
}
}
if (d.max !== undefined) {
this.hasRange = true;
if (domain.indexOf(d.max) < 0) {
domain.push(d.max);
}
}
}
}
const values = [...domain];
if (!this.autoScale) {
values.push(0);
}
const min = this.yScaleMin
? this.yScaleMin
: Math.min(...values);
const max = this.yScaleMax
? this.yScaleMax
: Math.max(...values);
return [min, max];
}
getSeriesDomain(): any[] {
return this.results.map(d => d.name);
}
getXScale(domain, width): any {
let scale;
if (this.scaleType === 'time') {
scale = scaleTime()
.range([0, width])
.domain(domain);
} else if (this.scaleType === 'linear') {
scale = scaleLinear()
.range([0, width])
.domain(domain);
if (this.roundDomains) {
scale = scale.nice();
}
} else if (this.scaleType === 'ordinal') {
scale = scalePoint()
.range([0, width])
.padding(0.1)
.domain(domain);
}
return scale;
}
getYScale(domain, height): any {
const scale = scaleLinear()
.range([height, 0])
.domain(domain);
return this.roundDomains ? scale.nice() : scale;
}
getScaleType(values): string {
let date = true;
let num = true;
for (const value of values) {
if (!this.isDate(value)) {
date = false;
}
if (typeof value !== 'number') {
num = false;
}
}
if (date) { return 'time'; }
if (num) { return 'linear'; }
return 'ordinal';
}
isDate(value): boolean {
if (value instanceof Date) {
return true;
}
return false;
}
updateDomain(domain): void {
this.filteredDomain = domain;
this.xDomain = this.filteredDomain;
this.xScale = this.getXScale(this.xDomain, this.dims.width);
}
updateHoveredVertical(item): void {
this.hoveredVertical = item.value;
this.deactivateAll();
}
@HostListener('mouseleave')
hideCircles(): void {
this.hoveredVertical = null;
this.deactivateAll();
}
onClick(data, series?): void {
if (series) {
data.series = series.name;
}
this.select.emit(data);
}
trackBy(index, item): string {
return item.name;
}
setColors(): void {
let domain;
if (this.schemeType === 'ordinal') {
domain = this.seriesDomain;
} else {
domain = this.yDomain;
}
this.colors = new ColorHelper(this.scheme, this.schemeType, domain, this.customColors);
}
getLegendOptions() {
const opts = {
scaleType: this.schemeType,
colors: undefined,
domain: [],
title: undefined
};
if (opts.scaleType === 'ordinal') {
opts.domain = this.seriesDomain;
opts.colors = this.colors;
opts.title = this.legendTitle;
} else {
opts.domain = this.yDomain;
opts.colors = this.colors.scale;
}
return opts;
}
updateYAxisWidth({ width }): void {
this.yAxisWidth = width;
this.update();
}
updateXAxisHeight({ height }): void {
this.xAxisHeight = height;
this.update();
}
onActivate(item) {
this.deactivateAll();
const idx = this.activeEntries.findIndex(d => {
return d.name === item.name && d.value === item.value;
});
if (idx > -1) {
return;
}
this.activeEntries = [item];
this.activate.emit({ value: item, entries: this.activeEntries });
}
onDeactivate(item) {
const idx = this.activeEntries.findIndex(d => {
return d.name === item.name && d.value === item.value;
});
this.activeEntries.splice(idx, 1);
this.activeEntries = [...this.activeEntries];
this.deactivate.emit({ value: item, entries: this.activeEntries });
}
deactivateAll() {
this.activeEntries = [...this.activeEntries];
for (const entry of this.activeEntries) {
this.deactivate.emit({ value: entry, entries: [] });
}
this.activeEntries = [];
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment