Creating Sparkline Chart Directive with ng2-nvd3
I’m attempting to create a Sparkline Chart because most typical chart libraries don’t support it, especially those compatible with Angular. I plan to build this chart myself and leverage ng2-nvd3
since I prefer charts rendered with this library. To ensure ease of reuse, I’ll implement it as an Angular Directive.
Prerequisites
To use nvd3 with Angular, you must install ng2-nvd3:
npm install ng2-nvd3
Key Features
- Sparkline Charts differentiate themselves by using a single line where areas above and below a threshold have different colors. Standard line charts lack this feature.
- The implementation strategy involves overlaying two charts. The fill area of one chart is inverted, and both are clipped based on the threshold value.
- Rather than relying on nvd3 commands, this directive directly renders SVG elements to the DOM.
Directive Parameters
These options are available when invoking the directive. Parameters without defaults are mandatory.
<div id="sparklineThreshold"
[sparklineThreshold]="sparklineThresholddata"
[height]="100"
[width]="400"
[type]="linear"
[threshold]="100"
[strokecolors]="['red','blue']"
[fillcolors]="['#da343452', '#c7daea']">
</div>
Parameter Details
id
: An ID is required for the DOM element to allow multiple directives to be used simultaneously on a single page.sparklineThreshold
: The directive name. Pass chart data as anArray<number>
.height
: The height of the SVG element.width
: The width of the SVG element, which determines the x-axis division based on the data length.type
: Eitherlinear
(default) for straight lines orbasis
for curved lines.threshold
: The reference point. The colors of the chart are determined based on this value.strokecolors
: An array containing the line colors for above and below the threshold. Defaults to['red','blue']
.fillcolors
: An array containing the fill colors for above and below the threshold. Defaults to['#da343452', '#c7daea']
.
Data Structure
threshold
: This value is constrained within the min and max values of the dataset, and then calculated as a percentage.
this.maxData = Math.max(...this.rawData);
this.minData = Math.min(...this.rawData);
// Ensure the threshold does not exceed max/min bounds
let threshold = (this.rawThreshold > this.maxData) ? this.maxData : this.rawThreshold;
threshold = (this.rawThreshold < this.minData) ? this.minData : this.rawThreshold;
this.threshold = Math.round((1 - (this.maxData - threshold) / this.maxData) * 100);
- x-axis: Based on the specified width.
- y-axis: Calculated as a percentage based on the maximum data value.
const dataLength = this.rawData.length;
const step = Math.round(this.width / this.rawData.length);
let i = 0;
for (const item of this.rawData) {
this.data.push({
x: i, y: Math.round((1 - (this.maxData - item) / this.maxData) * 100)
});
i += step;
}
Chart Rendering
- The process involves drawing two identical charts.
- The domain is set from the minimum to the maximum data value.
- The range spans from the specified height down to 0.
Upper Chart
- The
clipPath
has a y-coordinate of 0 and a height corresponding to the scaled threshold. - This
clipPath
is applied to the path. - Only the data above the threshold is visible.
Lower Chart
- The
clipPath
’s y-coordinate is set to the scaled threshold, with a height up to the maximum data value. - This
clipPath
is applied to the path. - Only the data below the threshold is visible.
Complete Source Code
import { Directive, Input, ViewContainerRef, AfterViewInit } from '@angular/core';
declare let d3: any;
@Directive({
selector: '[sparklineThreshold]'
})
export class SparkLineThresholdDirective implements AfterViewInit {
rawData = [];
data = [];
maxData = 0;
minData = 0;
maxWidth = 0;
rawThreshold = 0;
threshold = 0;
topStrokeColor = 'red';
bottomStrokeColor = 'blue';
topFillColor = '#da343452';
bottomFillColor = '#c7daea';
elementId;
// Actual data = Array of Numbers
@Input('sparklineThreshold') set setData(data: Array<number>) {
this.rawData = data;
// Test Data
// const numPoints = 50;
// for (let i = 1; i < numPoints; i++) {
// const rnd = Math.floor(Math.random() * (250)) + 1;
// this.rawData.push(rnd);
// }
}
@Input('width') width: number;
@Input('height') height: number;
@Input('type') type: string;
@Input('threshold') set setThreshold(threshold: number) { this.rawThreshold = threshold; }
@Input('strokecolors') set setStrokeTopColor(colors: Array<string>) {
this.topStrokeColor = colors[0];
this.bottomStrokeColor = colors[1];
}
@Input('fillcolors') set setFillColors(colors: Array<string>) {
this.topFillColor = colors[0];
this.bottomFillColor = colors[1];
}
constructor(private viewContainer: ViewContainerRef) {
this.elementId = this.viewContainer.element.nativeElement.id;
}
// Rawdata and ID are required
ngAfterViewInit() {
if (!this.rawData || !this.rawData.length) {
console.log('rawData are required');
return;
}
if (!this.elementId) {
console.error('element Id is required');
return;
}
// Initial values
this.maxData = Math.max(...this.rawData);
this.minData = Math.min(...this.rawData);
// threshold does not exceed the max range to chang
e the percentage
let threshold = (this.rawThreshold > this.maxData) ? this.maxData : this.rawThreshold;
threshold = (this.rawThreshold < this.minData) ? this.minData : this.rawThreshold;
this.threshold = (100 - Math.round(((this.maxData - threshold) / this.maxData) * 100));
this.type = this.type ? this.type : 'linear';
this.setSparklineData();
this.drawSparkLine();
// resize https://github.com/novus/nvd3/issues/645
}
// Mapping rawData
setSparklineData() {
const dataLength = this.rawData.length;
const step = Math.round(this.width / this.rawData.length);
let i = 0;
for (const item of this.rawData) {
this.maxWidth = step * i;
this.data.push({
x: this.maxWidth, y: (100 - Math.round(((this.maxData - item) / this.maxData) * 100))
});
i++;
}
}
// Drawing
drawSparkLine() {
const svg = d3.select(this.viewContainer.element.nativeElement).append('svg');
svg.attr('width', this.width).attr('height', this.height + 5);
// Add a margin of about 5 because there is an invisible margin.
// Otherwise, the end point may be hidden.
const draw = svg.append('g');
const yScale = d3.scale.linear()
.domain([this.minData, this.maxData])
.range([this.height, 0]);
// top
const setTopLine = d3.svg.area()
.x((d) => d.x)
.y((d) => yScale(d.y))
.y0(this.height)
.interpolate(this.type);
draw.append('clipPath')
.attr('id', this.elementId + '-top')
.append('rect')
.attr('x', 1) // Give 1 to hide the left starting point because it starts at 0 point.
.attr('y', 0)
.attr('rx', 0)
.attr('ry', 0)
.attr('height', yScale(this.threshold))
.attr('width', this.maxWidth - 2); // Subtract 2 to hide the part that converges to 0 at the end po
int.
draw.append('path')
.data([this.data])
.attr('d', setTopLine)
.attr('clip-path', 'url(#' + this.elementId + '-top)')
.attr('stroke', this.topStrokeColor)
.attr('stroke-width', 1)
.attr('fill', this.topFillColor);
// bottom
const setBottomLine = d3.svg.area()
.x((d) => d.x)
.y((d) => yScale(d.y))
.y0(this.threshold)
.interpolate(this.type);
draw.append('clipPath')
.attr('id', this.elementId + '-bottom')
.append('rect')
.attr('x', 1) // Give 1 to hide the left starting point because it starts at 0 point.
.attr('y', yScale(this.threshold))
.attr('rx', 0)
.attr('ry', 0)
.attr('height', this.maxData)
.attr('width', this.maxWidth - 2); // Subtract 2 to hide the part that converges to 0 at the end point.
draw.append('path')
.data([this.data])
.attr('d', setBottomLine)
.attr('clip-path', 'url(#' + this.elementId + '-bottom)')
.attr('stroke', this.bottomStrokeColor)
.attr('stroke-width', 1)
.attr('fill', this.bottomFillColor);
}
}
Complete.
댓글남기기