@nativescript-community/ui-chart
Version:
A powerful chart / graph plugin, supporting line, bar, pie, radar, bubble, and candlestick charts as well as scaling, panning and animations.
654 lines (653 loc) • 23.9 kB
JavaScript
import { arrayToNativeArray as arrayToNativeArrayFn, createArrayBuffer as createArrayBufferFn, createArrayBufferOrNativeArray as createArrayBufferOrNativeArrayFn, createNativeArray as createNativeArrayFn, nativeArrayToArray as nativeArrayToArrayFn, pointsFromBuffer as pointsFromBufferFn, supportsDirectArrayBuffers as supportsDirectArrayBuffersFn } from '@nativescript-community/arraybuffers';
import { Align, FontMetrics, LayoutAlignment, Matrix, Paint, Path, Rect, RectF, StaticLayout, Style } from '@nativescript-community/ui-canvas';
import { ObservableArray, Trace } from '@nativescript/core';
import { Screen } from '@nativescript/core/platform';
import { DefaultValueFormatter } from '../formatter/DefaultValueFormatter';
export const ChartTraceCategory = 'NativescriptChart';
export var CLogTypes;
(function (CLogTypes) {
CLogTypes[CLogTypes["log"] = 0] = "log";
CLogTypes[CLogTypes["info"] = 1] = "info";
CLogTypes[CLogTypes["warning"] = 2] = "warning";
CLogTypes[CLogTypes["error"] = 3] = "error";
})(CLogTypes || (CLogTypes = {}));
export const CLog = (type, ...args) => {
Trace.write(args.map((a) => (a && typeof a === 'object' ? JSON.stringify(a) : a)).join(' '), ChartTraceCategory, type);
};
let SDK_INT = -1;
function getSDK() {
if (SDK_INT === -1) {
SDK_INT = android.os.Build.VERSION.SDK_INT;
}
return SDK_INT;
}
/**
* Utilities class that has some helper methods. Needs to be initialized by
* calling Utils.init(...) before usage. Inside the Chart.init() method, this is
* done, if the Utils are used before that, Utils.init(...) needs to be called
* manually.
*
*/
export var Utils;
(function (Utils) {
// const mainScreen = screen.mainScreen;
Utils.density = Screen.mainScreen.scale;
// const mMetrics;
// const mMinimumFlingVelocity = 50;
// const mMaximumFlingVelocity = 8000;
// const DOUBLE_EPSILON = Number.EPSILON;
Utils.DEG2RAD = Math.PI / 180.0;
Utils.RAD2DEG = 180.0 / Math.PI;
Utils.NUMBER_EPSILON = Number.EPSILON;
const mDrawTextRectBuffer = new Rect(0, 0, 0, 0);
const mCalcTextHeightRect = new Rect(0, 0, 0, 0);
const mFontMetricsBuffer = new FontMetrics();
const mCalcTextSizeRect = new Rect(0, 0, 0, 0);
const mDefaultValueFormatter = generateDefaultValueFormatter();
/**
* Math.pow(...) is very expensive, so avoid calling it and create it
* yourself.
*/
const POW_10 = [1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000];
const EPSILON = Math.pow(2, -52);
const MAX_VALUE = (2 - EPSILON) * Math.pow(2, 1023);
const MIN_VALUE = Math.pow(2, -1022);
/**
* initialize method, called inside the Chart.init() method.
*
* @param context
*/
function init() { }
Utils.init = init;
/**
* This method converts dp unit to equivalent pixels, depending on device
* density. NEEDS UTILS TO BE INITIALIZED BEFORE USAGE.
*
* @param dp A value in dp (density independent pixels) unit. Which we need
* to convert into pixels
* @return A let value to represent px equivalent to dp depending on
* device density
*/
function convertDpToPixel(dp) {
return dp * Utils.density;
// return dp;
}
Utils.convertDpToPixel = convertDpToPixel;
/**
* This method converts device specific pixels to density independent
* pixels. NEEDS UTILS TO BE INITIALIZED BEFORE USAGE.
*
* @param px A value in px (pixels) unit. Which we need to convert into db
* @return A let value to represent dp equivalent to px value
*/
function convertPixelsToDp(px) {
return px;
// return px / density;
}
Utils.convertPixelsToDp = convertPixelsToDp;
/**
* calculates the approximate width of a text, depending on a demo text
* avoid repeated calls (e.g. inside drawing methods)
*
* @param paint
* @param demoText
* @return
*/
function calcTextWidth(paint, demoText) {
return paint.measureText(demoText);
}
Utils.calcTextWidth = calcTextWidth;
/**
* calculates the approximate height of a text, depending on a demo text
* avoid repeated calls (e.g. inside drawing methods)
*
* @param paint
* @param demoText
* @return
*/
function calcTextHeight(paint, demoText) {
mCalcTextHeightRect.set(0, 0, 0, 0);
paint.getTextBounds(demoText, 0, demoText.length, mCalcTextHeightRect);
return mCalcTextHeightRect.height();
}
Utils.calcTextHeight = calcTextHeight;
function getLineHeightFromMetrics(fontMetrics) {
return fontMetrics.descent - fontMetrics.ascent;
}
Utils.getLineHeightFromMetrics = getLineHeightFromMetrics;
function getLineHeight(paint, fontMetrics = mFontMetricsBuffer) {
paint.getFontMetrics(fontMetrics);
return getLineHeightFromMetrics(fontMetrics);
}
Utils.getLineHeight = getLineHeight;
function getLineSpacingFromMetrics(fontMetrics) {
return fontMetrics.ascent - fontMetrics.top + fontMetrics.bottom;
}
Utils.getLineSpacingFromMetrics = getLineSpacingFromMetrics;
function getLineSpacing(paint, fontMetrics = mFontMetricsBuffer) {
paint.getFontMetrics(fontMetrics);
return getLineSpacingFromMetrics(fontMetrics);
}
Utils.getLineSpacing = getLineSpacing;
/**
* calculates the approximate size of a text, depending on a demo text
* avoid repeated calls (e.g. inside drawing methods)
*
* @param paint
* @param demoText
* @param outputFSize An output variable, modified by the function.
*/
function calcTextSize(paint, demoText) {
const r = mCalcTextSizeRect;
r.set(0, 0, 0, 0);
paint.getTextBounds(demoText, 0, demoText.length, r);
return {
width: r.width(),
height: r.height()
};
}
Utils.calcTextSize = calcTextSize;
function generateDefaultValueFormatter() {
return new DefaultValueFormatter(1);
}
Utils.generateDefaultValueFormatter = generateDefaultValueFormatter;
/// - returns: The default value formatter used for all chart components that needs a default
function getDefaultValueFormatter() {
return mDefaultValueFormatter;
}
Utils.getDefaultValueFormatter = getDefaultValueFormatter;
/**
* Formats the given number to the given number of decimals, and returns the
* number as a string, maximum 35 characters.
*
* @param number
* @param digitCount
* @param separateThousands set this to true to separate thousands values
* @param separateChar a caracter to be paced between the "thousands"
* @return
*/
function formatNumber(number, digitCount, separateThousands, separateChar = '.') {
const out = [];
let neg = false;
if (number === 0) {
return '0';
}
let zero = false;
if (number < 1 && number > -1) {
zero = true;
}
if (number < 0) {
neg = true;
number = -number;
}
if (digitCount > POW_10.length) {
digitCount = POW_10.length - 1;
}
number *= POW_10[digitCount];
let lval = Math.round(number);
let ind = out.length - 1;
let charCount = 0;
let decimalPointAdded = false;
while (lval !== 0 || charCount < digitCount + 1) {
const digit = lval % 10;
lval = lval / 10;
out[ind--] = digit + '0';
charCount++;
// add decimal point
if (charCount === digitCount) {
out[ind--] = ',';
charCount++;
decimalPointAdded = true;
// add thousand separators
}
else if (separateThousands && lval !== 0 && charCount > digitCount) {
if (decimalPointAdded) {
if ((charCount - digitCount) % 4 === 0) {
out[ind--] = separateChar;
charCount++;
}
}
else {
if ((charCount - digitCount) % 4 === 3) {
out[ind--] = separateChar;
charCount++;
}
}
}
}
// if number around zero (between 1 and -1)
if (zero) {
out[ind--] = '0';
charCount += 1;
}
// if the number is negative
if (neg) {
out[ind--] = '-';
charCount += 1;
}
const start = out.length - charCount;
return out.slice(out.length - start).join('');
}
Utils.formatNumber = formatNumber;
/**
* rounds the given number to the next significant number
*
* @param number
* @return
*/
function roundToNextSignificant(number) {
if (!Number.isFinite(number) || isNaN(number) || number === 0.0)
return 0;
const d = Math.ceil(Math.log10(number < 0 ? -number : number));
const pw = 1 - d;
const magnitude = Math.pow(10, pw);
const shifted = Math.round(number * magnitude);
return shifted / magnitude;
}
Utils.roundToNextSignificant = roundToNextSignificant;
/**
* Returns the appropriate number of decimals to be used for the provided
* number.
*
* @param number
* @return
*/
function getDecimals(number) {
const i = roundToNextSignificant(number);
if (!Number.isFinite(i) || i === 0)
return 0;
return Math.ceil(-Math.log10(i)) + 2;
}
Utils.getDecimals = getDecimals;
/**
* Replacement for the Math.nextUp(...) method that is only available in
* HONEYCOMB and higher. Dat's some seeeeek sheeet.
*
* @param d
* @return
*/
function nextUp(x) {
if (x !== x) {
return x;
}
if (x === -1 / 0) {
return -MAX_VALUE;
}
if (x === +1 / 0) {
return +1 / 0;
}
if (x === +MAX_VALUE) {
return +1 / 0;
}
let y = x * (x < 0 ? 1 - EPSILON / 2 : 1 + EPSILON);
if (y === x) {
y = MIN_VALUE * EPSILON > 0 ? x + MIN_VALUE * EPSILON : x + MIN_VALUE;
}
if (y === +1 / 0) {
y = +MAX_VALUE;
}
const b = x + (y - x) / 2;
if (x < b && b < y) {
y = b;
}
const c = (y + x) / 2;
if (x < c && c < y) {
y = c;
}
return y === 0 ? -0 : y;
}
Utils.nextUp = nextUp;
function toRadians(degrees) {
const pi = Math.PI;
return degrees * (pi / 180);
}
Utils.toRadians = toRadians;
/**
* Returns a recyclable MPPointF instance.
* Calculates the position around a center point, depending on the distance
* from the center, and the angle of the position around the center.
*
* @param center
* @param dist
* @param angle in degrees, converted to radians internally
* @return
*/
function getPosition(center, dist, angle, outputPoint) {
if (outputPoint) {
outputPoint.x = center.x + dist * Math.cos(toRadians(angle));
outputPoint.y = center.y + dist * Math.sin(toRadians(angle));
return outputPoint;
}
return {
x: center.x + dist * Math.cos(toRadians(angle)),
y: center.y + dist * Math.sin(toRadians(angle))
};
}
Utils.getPosition = getPosition;
/**
* returns an angle between 0 < 360 (not less than zero, less than 360)
*/
function getNormalizedAngle(angle) {
while (angle < 0)
angle += 360;
return angle % 360;
}
Utils.getNormalizedAngle = getNormalizedAngle;
// const mDrawableBoundsCache = new Rect(0, 0, 0, 0);
// export function drawImage(canvas: Canvas, drawable: ImageSource, x, y, width, height) {
// const drawOffsetx = x - width / 2;
// const drawOffsety = y - height / 2;
// drawable.copyBounds(mDrawableBoundsCache);
// drawable.setBounds(this.mDrawableBoundsCache.left, this.mDrawableBoundsCache.top, this.mDrawableBoundsCache.left + width, this.mDrawableBoundsCache.top + width);
// canvas.save();
// // translate to the correct position and draw
// canvas.translate(drawOffsetx, drawOffsety);
// drawable.draw(canvas);
// canvas.restore();
// }
function drawXAxisValue(c, text, x, y, paint, anchor, angleDegrees) {
let drawOffsetX = 0;
let drawOffsetY = 0;
// Android does not snap the bounds to line boundaries,
// and draws from bottom to top.
// And we want to normalize it.
drawOffsetY += -mFontMetricsBuffer.ascent;
if (angleDegrees !== 0) {
const lineHeight = getLineHeightFromMetrics(mFontMetricsBuffer);
paint.getTextBounds(text, 0, text.length, mDrawTextRectBuffer);
// Move the text drawing rect in a way that it always rotates around its center
drawOffsetX -= mDrawTextRectBuffer.width() * 0.5;
drawOffsetY -= lineHeight * 0.5;
let translateX = x;
let translateY = y;
// Move the "outer" rect relative to the anchor, assuming its centered
if (anchor.x !== 0.5 || anchor.y !== 0.5) {
const rotatedSize = getSizeOfRotatedRectangleByDegrees(mDrawTextRectBuffer.width(), lineHeight, angleDegrees);
translateX -= rotatedSize.width * (anchor.x - 0.5);
translateY -= rotatedSize.height * (anchor.y - 0.5);
}
c.save();
c.translate(translateX, translateY);
c.rotate(angleDegrees);
c.drawText(text, drawOffsetX, drawOffsetY, paint);
c.restore();
}
else {
if (anchor.y !== 0) {
const lineHeight = getLineHeightFromMetrics(mFontMetricsBuffer);
drawOffsetY -= lineHeight * anchor.y;
}
drawOffsetX += x;
drawOffsetY += y;
c.drawText(text, drawOffsetX, drawOffsetY, paint);
}
// paint.setTextAlign(originalTextAlign);
}
Utils.drawXAxisValue = drawXAxisValue;
function drawMultilineText(c, textLayout, x, y, anchor, angleDegrees, lineHeight) {
let drawOffsetX = 0;
let drawOffsetY = 0;
const drawWidth = textLayout.getWidth();
const drawHeight = textLayout.getLineCount() * lineHeight;
// Android sometimes has pre-padding
drawOffsetX -= mDrawTextRectBuffer.left;
// Android does not snap the bounds to line boundaries,
// and draws from bottom to top.
// And we want to normalize it.
drawOffsetY += drawHeight;
// To have a consistent point of reference, we always draw left-aligned
// Paint.Align originalTextAlign = paint.getTextAlign();
// paint.setTextAlign(Paint.Align.LEFT);
if (angleDegrees !== 0) {
// Move the text drawing rect in a way that it always rotates around its center
drawOffsetX -= drawWidth * 0.5;
drawOffsetY -= drawHeight * 0.5;
let translateX = x;
let translateY = y;
// Move the "outer" rect relative to the anchor, assuming its centered
if (anchor.x !== 0.5 || anchor.y !== 0.5) {
const rotatedSize = getSizeOfRotatedRectangleByDegrees(drawWidth, drawHeight, angleDegrees);
translateX -= rotatedSize.width * (anchor.x - 0.5);
translateY -= rotatedSize.height * (anchor.y - 0.5);
// FSize.recycleInstance(rotatedSize);
}
c.save();
c.translate(translateX, translateY);
c.rotate(angleDegrees);
c.translate(drawOffsetX, drawOffsetY);
textLayout.draw(c);
c.restore();
}
else {
if (anchor.x !== 0 || anchor.y !== 0) {
drawOffsetX -= drawWidth * anchor.x;
drawOffsetY -= drawHeight * anchor.y;
}
drawOffsetX += x;
drawOffsetY += y;
c.save();
c.translate(drawOffsetX, drawOffsetY);
textLayout.draw(c);
c.restore();
}
// paint.setTextAlign(originalTextAlign);
}
Utils.drawMultilineText = drawMultilineText;
function drawMultilineTextConstrained(c, text, x, y, paint, constrainedToSize, anchor, angleDegrees, lineHeight) {
const textLayout = new StaticLayout(text, paint, Math.max(Math.ceil(constrainedToSize.width), 1), LayoutAlignment.ALIGN_NORMAL, 1, 0, false);
drawMultilineText(c, textLayout, x, y, anchor, angleDegrees, lineHeight);
}
Utils.drawMultilineTextConstrained = drawMultilineTextConstrained;
/**
* Returns a recyclable FSize instance.
* Represents size of a rotated rectangle by degrees.
*
* @param rectangleWidth
* @param rectangleHeight
* @param degrees
* @return A Recyclable FSize instance
*/
function getSizeOfRotatedRectangleByDegrees(rectangleWidth, rectangleHeight, degrees) {
const radians = degrees * Utils.DEG2RAD;
return getSizeOfRotatedRectangleByRadians(rectangleWidth, rectangleHeight, radians);
}
Utils.getSizeOfRotatedRectangleByDegrees = getSizeOfRotatedRectangleByDegrees;
/**
* Returns a recyclable FSize instance.
* Represents size of a rotated rectangle by radians.
*
* @param rectangleWidth
* @param rectangleHeight
* @param radians
* @return A Recyclable FSize instance
*/
function getSizeOfRotatedRectangleByRadians(rectangleWidth, rectangleHeight, radians) {
return {
width: Math.abs(rectangleWidth * Math.cos(radians)) + Math.abs(rectangleHeight * Math.sin(radians)),
height: Math.abs(rectangleWidth * Math.sin(radians)) + Math.abs(rectangleHeight * Math.cos(radians))
};
}
Utils.getSizeOfRotatedRectangleByRadians = getSizeOfRotatedRectangleByRadians;
Utils.supportsDirectArrayBuffers = supportsDirectArrayBuffersFn;
Utils.createArrayBuffer = createArrayBufferFn;
Utils.pointsFromBuffer = pointsFromBufferFn;
Utils.createArrayBufferOrNativeArray = createArrayBufferOrNativeArrayFn;
Utils.createNativeArray = createNativeArrayFn;
Utils.nativeArrayToArray = nativeArrayToArrayFn;
Utils.arrayToNativeArray = arrayToNativeArrayFn;
const mTempArrays = {};
function getTempArray(length, useInts = false, canReturnBuffer = true, optKey) {
let key = length + '' + useInts + '' + canReturnBuffer;
if (optKey) {
key += optKey;
}
if (mTempArrays[key]) {
return mTempArrays[key];
}
const buf = (mTempArrays[key] = Utils.createArrayBuffer(length, useInts, canReturnBuffer));
return buf;
}
Utils.getTempArray = getTempArray;
let mTempRectF;
function getTempRectF() {
if (!mTempRectF) {
mTempRectF = new RectF(0, 0, 0, 0);
}
return mTempRectF;
}
Utils.getTempRectF = getTempRectF;
let mTempRect;
function getTempRect() {
if (!mTempRect) {
mTempRect = new Rect(0, 0, 0, 0);
}
return mTempRect;
}
Utils.getTempRect = getTempRect;
let mTempPath;
function getTempPath() {
if (!mTempPath) {
mTempPath = new Path();
}
return mTempPath;
}
Utils.getTempPath = getTempPath;
let mTempMatrix;
function getTempMatrix() {
if (!mTempMatrix) {
mTempMatrix = new Matrix();
}
return mTempMatrix;
}
Utils.getTempMatrix = getTempMatrix;
let mTempPaint;
function getTempPaint() {
if (!mTempPaint) {
mTempPaint = new Paint();
}
return mTempPaint;
}
Utils.getTempPaint = getTempPaint;
const mTemplatePaints = {};
function getTemplatePaint(template) {
// const cached = mTemplatePaints[template];
// for now cached paint share the same font object when cloned so it is no good
// if (cached) {
// return new Paint(cached);
// }
let paint;
switch (template) {
case 'black-stroke': {
paint = new Paint();
paint.setStyle(Style.STROKE);
paint.setColor('black');
paint.setStrokeWidth(1);
break;
}
case 'gray-stroke': {
paint = getTemplatePaint('black-stroke');
paint.setColor('gray');
break;
}
case 'white-stroke': {
paint = getTemplatePaint('black-stroke');
paint.setColor('white');
break;
}
case 'black-fill': {
paint = new Paint();
paint.setColor('black');
paint.setStyle(Style.FILL);
break;
}
case 'white-fill': {
paint = getTemplatePaint('black-fill');
paint.setColor('white');
break;
}
case 'grid': {
paint = getTemplatePaint('black-stroke');
this.mGridPaint = new Paint();
paint.setColor('gray');
paint.setAlpha(90);
break;
}
case 'value': {
paint = new Paint();
paint.setColor('#3F3F3F');
paint.setTextAlign(Align.CENTER);
paint.setTextSize(9);
break;
}
}
mTemplatePaints[template] = paint;
return new Paint(paint);
}
Utils.getTemplatePaint = getTemplatePaint;
function clipPathSupported() {
if (__ANDROID__) {
return getSDK() >= 18;
}
return true;
}
Utils.clipPathSupported = clipPathSupported;
/**
* Calculates the sum across all values of the given array.
*
* @param values
* @return
*/
function calcSum(values) {
let sum = 0;
if (!values) {
return sum;
}
for (const f of values) {
sum += f;
}
return sum;
}
Utils.calcSum = calcSum;
/**
* Calculates the sum of positive numbers and negative numbers separately,
* across all values of the given array.
*
* @param values
* @return
*/
function calcPosNegSum(values) {
const sums = { pos: 0, neg: 0 };
if (!values) {
return sums;
}
const sumNeg = 0;
const sumPos = 0;
for (const f of values) {
if (f <= 0) {
sums.neg += Math.abs(f);
}
else {
sums.pos += f;
}
}
return sums;
}
Utils.calcPosNegSum = calcPosNegSum;
function calcSumToIndex(index, values, desc) {
if (!values)
return 0;
let remainder = 0;
let lastIndex = values.length - 1;
while (lastIndex > index && lastIndex >= 0) {
remainder += values[index];
lastIndex--;
}
return remainder;
}
Utils.calcSumToIndex = calcSumToIndex;
function getArrayItem(array, index) {
return array instanceof ObservableArray ? array.getItem(index) : array[index];
}
Utils.getArrayItem = getArrayItem;
})(Utils || (Utils = {}));
//# sourceMappingURL=Utils.js.map