Created
September 28, 2018 13:52
-
-
Save gilmarsquinelato/c18000ec3344bb6c4c8465550fed80f8 to your computer and use it in GitHub Desktop.
ngx-charts realtime line chart component possible solution
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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