lemon-ngx-trend
Version:
ngx-trend Angular component for Lemoncloud
396 lines (387 loc) • 18.3 kB
JavaScript
import { __read, __spread, __decorate, __metadata } from 'tslib';
import { trigger, state, style, transition, animate, keyframes } from '@angular/animations';
import { Input, ViewChild, ElementRef, Component, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
/* eslint-disable no-restricted-properties */
/** normalize
* This lets us translate a value from one scale to another.
*
* @param value - Our initial value to translate
* @param min - the current minimum value possible
* @param max - the current maximum value possible
* @param scaleMin - the min value of the scale we're translating to
* @param scaleMax - the max value of the scale we're translating to
*
* @returns the value on its new scale
*/
function normalize(value, min, max, scaleMin, scaleMax) {
if (scaleMin === void 0) { scaleMin = 0; }
if (scaleMax === void 0) { scaleMax = 1; }
// If the `min` and `max` are the same value, it means our dataset is flat.
// For now, let's assume that flat data should be aligned to the bottom.
if (min === max) {
return scaleMin;
}
return scaleMin + (value - min) * (scaleMax - scaleMin) / (max - min);
}
/** moveTo
* the coordinate that lies at a midpoint between 2 lines, based on the radius
*
* @param to - Our initial point
* @param to.x - The x value of our initial point
* @param to.y - The y value of our initial point
* @param from - Our final point
* @param from.x - The x value of our final point
* @param from.y - The y value of our final point
* @param radius - The distance away from the final point
*
* @returns an object holding the x/y coordinates of the midpoint.
*/
function moveTo(to, from, radius) {
var length = Math.sqrt((to.x - from.x) * (to.x - from.x) + (to.y - from.y) * (to.y - from.y));
var unitVector = { x: (to.x - from.x) / length, y: (to.y - from.y) / length };
return {
x: from.x + unitVector.x * radius,
y: from.y + unitVector.y * radius,
};
}
/** getDistanceBetween
* Simple formula derived from pythagoras to calculate the distance between
* 2 points on a plane.
*
* @param p1 - Our initial point
* @param p1.x - The x value of our initial point
* @param p1.y - The y value of our initial point
* @param p2 - Our final point
* @param p2.x - The x value of our final point
* @param p2.y - The y value of our final point
*
* @returns the distance between the points.
*/
var getDistanceBetween = function (p1, p2) {
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
};
/** checkForCollinearPoints
* Figure out if the midpoint fits perfectly on a line between the two others.
*
* @param p1 - Our initial point
* @param p1.x - The x value of our initial point
* @param p1.y - The y value of our initial point
* @param p2 - Our mid-point
* @param p2.x - The x value of our mid-point
* @param p2.y - The y value of our mid-point
* @param p3 - Our final point
* @param p3.x - The x value of our final point
* @param p3.y - The y value of our final point
* @returns whether or not p2 sits on the line between p1 and p3.
*/
var checkForCollinearPoints = function (p1, p2, p3) {
return (p1.y - p2.y) * (p1.x - p3.x) === (p1.y - p3.y) * (p1.x - p2.x);
};
var buildLinearPath = function (data) {
return data.reduce(function (path, point, index) {
// The very first instruction needs to be a "move".
// The rest will be a "line".
var isFirstInstruction = index === 0;
var instruction = isFirstInstruction ? 'M' : 'L';
return "" + path + instruction + " " + point.x + "," + point.y + "\n";
}, '');
};
function buildSmoothPath(data, radius) {
var _a = __read(data), firstPoint = _a[0], otherPoints = _a.slice(1);
return otherPoints.reduce(function (path, point, index) {
var next = otherPoints[index + 1];
var prev = otherPoints[index - 1] || firstPoint;
var isCollinear = next && checkForCollinearPoints(prev, point, next);
if (!next || isCollinear) {
// The very last line in the sequence can just be a regular line.
return path + "\nL " + point.x + "," + point.y;
}
var distanceFromPrev = getDistanceBetween(prev, point);
var distanceFromNext = getDistanceBetween(next, point);
var threshold = Math.min(distanceFromPrev, distanceFromNext);
var isTooCloseForRadius = threshold / 2 < radius;
var radiusForPoint = isTooCloseForRadius ? threshold / 2 : radius;
var before = moveTo(prev, point, radiusForPoint);
var after = moveTo(next, point, radiusForPoint);
return [
path,
"L " + before.x + "," + before.y,
"S " + point.x + "," + point.y + " " + after.x + "," + after.y,
].join('\n');
}, "M " + firstPoint.x + "," + firstPoint.y);
}
var generateId = function () { return Math.round(Math.random() * Math.pow(10, 16)); };
function normalizeDataset(data, minX, maxX, minY, maxY, minValue, maxValue) {
// For the X axis, we want to normalize it based on its index in the array.
// For the Y axis, we want to normalize it based on the element's value.
//
// X axis is easy: just evenly-space each item in the array.
// For the Y axis, we first need to find the min and max of our array,
// and then normalize those values between 0 and 1.
var boundariesX = { min: 0, max: data.length - 1 };
var boundariesY = { min: minValue || Math.min.apply(Math, __spread(data)), max: maxValue || Math.max.apply(Math, __spread(data)) };
var normalizedData = data.map(function (point, index) { return ({
x: normalize(index, boundariesX.min, boundariesX.max, minX, maxX),
y: normalize(point, boundariesY.min, boundariesY.max, minY, maxY),
}); });
// According to the SVG spec, paths with a height/width of `0` can't have
// linear gradients applied. This means that our lines are invisible when
// the dataset is flat (eg. [0, 0, 0, 0]).
//
// The hacky solution is to apply a very slight offset to the first point of
// the dataset. As ugly as it is, it's the best solution we can find (there
// are ways within the SVG spec of changing it, but not without causing
// breaking changes).
if (boundariesY.min === boundariesY.max) {
normalizedData[0].y += 0.0001;
}
return normalizedData;
}
var TrendComponent = /** @class */ (function () {
function TrendComponent() {
this.autoDraw = false;
this.autoDrawDuration = 2000;
this.autoDrawEasing = 'ease';
this.padding = 8;
this.radius = 10;
this.stroke = 'black';
this.strokeLinecap = '';
this.strokeWidth = 1;
this.gradient = [];
this.svgHeight = '25%';
this.svgWidth = '100%';
// Added for Circle
this.showCircle = false;
this.circleColor = 'black';
this.circleWidth = 1;
this.showLastLabel = false;
this.labelColor = 'black';
this.animationState = '';
this.id = generateId();
this.gradientId = "ngx-trend-vertical-gradient-" + this.id;
}
TrendComponent.prototype.ngOnChanges = function () {
var _this = this;
// We need at least 2 points to draw a graph.
if (!this.data || this.data.length < 2) {
return;
}
// `data` can either be an array of numbers:
// [1, 2, 3]
// or, an array of objects containing a value:
// [{ value: 1 }, { value: 2 }, { value: 3 }]
//
// For now, we're just going to convert the second form to the first.
// Later on, if/when we support tooltips, we may adjust.
var plainValues = this.data.map(function (point) {
if (typeof point === 'number') {
return point;
}
return point.value;
});
// reset to re-run animation
this.animationState = 'inactive';
// Our viewbox needs to be in absolute units, so we'll default to 300x75
// Our SVG can be a %, though; this is what makes it scalable.
// By defaulting to percentages, the SVG will grow to fill its parent
// container, preserving a 1/4 aspect ratio.
var viewBoxWidth = this.width || 300;
var viewBoxHeight = this.height || 75;
this.svgWidth = this.width || '100%';
this.svgHeight = this.height || '25%';
this.viewBox = "0 0 " + viewBoxWidth + " " + viewBoxHeight;
var root = location.href.split(location.hash || '#')[0];
this.pathStroke = (this.gradient && this.gradient.length) ? "url('" + root + "#" + this.gradientId + "')" : undefined;
this.gradientTrimmed = this.gradient.slice().reverse().map(function (val, idx) {
return {
idx: idx,
stopColor: val,
offset: normalize(idx, 0, _this.gradient.length - 1 || 1),
};
});
var normalizedValues = normalizeDataset(plainValues, this.padding, viewBoxWidth - this.padding,
// NOTE: Because SVGs are indexed from the top left, but most data is
// indexed from the bottom left, we're inverting the Y min/max.
viewBoxHeight - this.padding, this.padding, this.minValue, this.maxValue);
this.circleCoordinates = normalizedValues;
this.lastLabelCoordinates = this.getLabelCoordinateOfLast();
if (this.autoDraw && this.animationState !== 'active') {
this.animationState = 'inactive';
setTimeout(function () {
_this.lineLength = _this.pathEl.nativeElement.getTotalLength();
_this.animationState = 'active';
});
}
this.d = this.smooth
? buildSmoothPath(normalizedValues, this.radius)
: buildLinearPath(normalizedValues);
};
TrendComponent.prototype.getLabelCoordinateOfLast = function () {
var lastIndex = this.circleCoordinates.length - 1;
var _a = this.circleCoordinates[lastIndex], x = _a.x, y = _a.y;
var result = {
x: x,
y: y + 15
};
return result;
};
__decorate([
Input(),
__metadata("design:type", Array)
], TrendComponent.prototype, "data", void 0);
__decorate([
Input(),
__metadata("design:type", Boolean)
], TrendComponent.prototype, "smooth", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], TrendComponent.prototype, "autoDraw", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], TrendComponent.prototype, "autoDrawDuration", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], TrendComponent.prototype, "autoDrawEasing", void 0);
__decorate([
Input(),
__metadata("design:type", Number)
], TrendComponent.prototype, "width", void 0);
__decorate([
Input(),
__metadata("design:type", Number)
], TrendComponent.prototype, "height", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], TrendComponent.prototype, "padding", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], TrendComponent.prototype, "radius", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], TrendComponent.prototype, "stroke", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], TrendComponent.prototype, "strokeLinecap", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], TrendComponent.prototype, "strokeWidth", void 0);
__decorate([
Input(),
__metadata("design:type", Array)
], TrendComponent.prototype, "gradient", void 0);
__decorate([
Input(),
__metadata("design:type", String)
], TrendComponent.prototype, "preserveAspectRatio", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], TrendComponent.prototype, "svgHeight", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], TrendComponent.prototype, "svgWidth", void 0);
__decorate([
ViewChild('pathEl'),
__metadata("design:type", ElementRef)
], TrendComponent.prototype, "pathEl", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], TrendComponent.prototype, "showCircle", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], TrendComponent.prototype, "circleColor", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], TrendComponent.prototype, "circleWidth", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], TrendComponent.prototype, "showLastLabel", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], TrendComponent.prototype, "labelColor", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], TrendComponent.prototype, "maxValue", void 0);
__decorate([
Input(),
__metadata("design:type", Object)
], TrendComponent.prototype, "minValue", void 0);
TrendComponent = __decorate([
Component({
selector: 'ngx-trend',
template: "\n <svg *ngIf=\"data && data.length >= 2\"\n [attr.width]=\"svgWidth\"\n [attr.height]=\"svgHeight\"\n [attr.stroke]=\"stroke\"\n [attr.stroke-width]=\"strokeWidth\"\n [attr.stroke-linecap]=\"strokeLinecap\"\n [attr.viewBox]=\"viewBox\"\n [attr.preserveAspectRatio]=\"preserveAspectRatio\"\n >\n <defs *ngIf=\"gradient && gradient.length\">\n <linearGradient [attr.id]=\"gradientId\" x1=\"0%\" y1=\"0%\" x2=\"0%\" y2=\"100%\">\n <stop\n *ngFor=\"let g of gradientTrimmed;\"\n [attr.key]=\"g.idx\"\n [attr.offset]=\"g.offset\"\n [attr.stop-color]=\"g.stopColor\"\n />\n </linearGradient>\n </defs>\n <path fill=\"none\" #pathEl\n [attr.stroke]=\"pathStroke\" [attr.d]=\"d\"\n [@pathAnimation]=\"{\n value: animationState,\n params: {\n autoDrawDuration: autoDrawDuration,\n autoDrawEasing: autoDrawEasing,\n lineLength: lineLength\n }\n }\" />\n <ng-container *ngIf=\"showCircle\" >\n <style>\n .small { font-size: 12px; font-weight: 400; text-anchor: end; stroke-width: 0;}\n </style>\n <ng-container *ngFor=\"let circle of circleCoordinates; index as i\">\n <circle [attr.cx]=\"circle.x\" [attr.cy]=\"circle.y\" [attr.r]=\"circleWidth\"\n [attr.fill]=\"circleColor\" [attr.stroke]=\"circleColor\"\n [attr.strokeWidth]=\"circleWidth\"\n [@circleAnimation]=\"{\n value: animationState,\n params: {\n autoDrawDuration: autoDrawDuration\n }\n }\"\n />\n <text *ngIf=\"showLastLabel && i === circleCoordinates.length - 1\"\n class=\"small\"\n [@circleAnimation]=\"{\n value: animationState,\n params: {\n autoDrawDuration: autoDrawDuration\n }\n }\"\n [attr.fill]=\"labelColor\"\n [attr.x]=\"lastLabelCoordinates.x\"\n [attr.y]=\"lastLabelCoordinates.y\">{{ data[data.length-1] | number }}</text>\n </ng-container>\n </ng-container>\n </svg>\n ",
animations: [
trigger('pathAnimation', [
state('inactive', style({ display: 'none' })),
transition('* => active', [
style({ display: 'initial' }),
// We do the animation using the dash array/offset trick
// https://css-tricks.com/svg-line-animation-works/
animate('{{ autoDrawDuration }}ms {{ autoDrawEasing }}', keyframes([
style({
'stroke-dasharray': '{{ lineLength }}px',
'stroke-dashoffset': '{{ lineLength }}px',
}),
style({
'stroke-dasharray': '{{ lineLength }}px',
'stroke-dashoffset': 0,
}),
])),
// One unfortunate side-effect of the auto-draw is that the line is
// actually 1 big dash, the same length as the line itself. If the
// line length changes (eg. radius change, new data), that dash won't
// be the same length anymore. We can fix that by removing those
// properties once the auto-draw is completed.
style({
'stroke-dashoffset': '',
'stroke-dasharray': '',
}),
]),
]),
trigger('circleAnimation', [
state('inactive', style({ visibility: 'hidden', opacity: 0 })),
transition('* => active', [
style({ visibility: 'hidden' }),
animate('{{ autoDrawDuration }}ms', keyframes([style({ visibility: 'visible' }),
])),
]),
])
]
}),
__metadata("design:paramtypes", [])
], TrendComponent);
return TrendComponent;
}());
var TrendModule = /** @class */ (function () {
function TrendModule() {
}
TrendModule = __decorate([
NgModule({
imports: [CommonModule],
exports: [TrendComponent],
declarations: [TrendComponent],
})
], TrendModule);
return TrendModule;
}());
/**
* Generated bundle index. Do not edit.
*/
export { TrendComponent, TrendModule };
//# sourceMappingURL=lemon-ngx-trend.js.map