UNPKG

lemon-ngx-trend

Version:

ngx-trend Angular component for Lemoncloud

396 lines (387 loc) 18.3 kB
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