Skip to content

Commit

Permalink
Fix/charts trendline (#4083)
Browse files Browse the repository at this point in the history
[all][trendline] trendine: obtainEquation fixed

* [all][trendline] Duplicate coordinate problem fixed

* [all][charts] Extremum all charts now is called only once, from chartSpace preCalculateData

* [all][trendline] Tails fixed for logarithmic trendlines

* [all][trendline] Cat axis boundaries now fixed to be integers

* [all][trendline] GetAdditionalInfo fixed for coordinate out of bounds scenarios

* [all][trendlines] DispRSquared formula changed, CTrendData new attribute isReversed added

* [all][trendline] GetAdditionalInfo out of bounds issue fixed to the top left corner

---------

Co-authored-by: ansaraidarbek <[email protected]>
  • Loading branch information
GoshaZotov and ansaraidarbek authored Dec 26, 2023
1 parent a25d9b5 commit fa94f73
Showing 1 changed file with 124 additions and 48 deletions.
172 changes: 124 additions & 48 deletions common/Charts/ChartsDrawer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1347,7 +1347,7 @@ CChartsDrawer.prototype =
//****calculate properties****
_calculateProperties: function (chartSpace) {
if (!this.calcProp.scale) {
this.preCalculateData(chartSpace);
this.preCalculateData(chartSpace, true);
}

//считаем маргины
Expand Down Expand Up @@ -2405,7 +2405,7 @@ CChartsDrawer.prototype =


//****functions for UP Functions****
preCalculateData: function (chartSpace) {
preCalculateData: function (chartSpace, notCalcExtremum) {
this._calculateChangeAxisMap(chartSpace);
this.cChartSpace = chartSpace;
this.calcProp.pxToMM = 1 / AscCommon.g_dKoef_pix_to_mm;
Expand All @@ -2420,7 +2420,9 @@ CChartsDrawer.prototype =
this.calcProp.yaxispos = null;

//рассчёт данных и ещё некоторых параметров(this.calcProp./min/max/ymax/ymin/)
this._calculateExtremumAllCharts(chartSpace);
if (!notCalcExtremum) {
this._calculateExtremumAllCharts(chartSpace);
}

//***series***
this.calcProp.series = chartSpace.chart.plotArea.chart.series;
Expand Down Expand Up @@ -16246,6 +16248,7 @@ CColorObj.prototype =
//control trend calculate type
this.bAllowDrawByBezier = true;
this.bAllowDrawByPoints = false;
this.continueAdding = true;
}

CTrendline.prototype = {
Expand All @@ -16268,10 +16271,18 @@ CColorObj.prototype =
if (!this.storage[chartId][seriaId]) {
this.storage[chartId][seriaId] = new CTrendData();
}
this.storage[chartId][seriaId].addCatVal(xVal);
this.storage[chartId][seriaId].addValVal(yVal);
if (yVal > 0) {
this.storage[chartId][seriaId].setMinLogVal(yVal);
if (!this.storage[chartId][seriaId].isEmpty() && xVal === this.storage[chartId][seriaId].coords.catVals[0]) {
this.continueAdding = false;
}

// in the case of duplicated data, no further adding should be allowed
if (this.continueAdding) {
this.storage[chartId][seriaId].addCatVal(xVal);
this.storage[chartId][seriaId].addValVal(yVal);

if (yVal > 0) {
this.storage[chartId][seriaId].setMinLogVal(yVal);
}
}
},

Expand All @@ -16280,6 +16291,20 @@ CColorObj.prototype =
const coefficients = this.storage[chartId][seriesId].getCoefficients();
const rSquared = this.storage[chartId][seriesId].getRSquared();
const lastPoint = this.storage[chartId][seriesId].getLastPoint();

let startCat = this.cChartDrawer.calcProp.chartGutter._left / this.cChartDrawer.calcProp.pxToMM;
let startVal = (this.cChartDrawer.calcProp.chartGutter._top) / this.cChartDrawer.calcProp.pxToMM;
let endCat = (this.cChartDrawer.calcProp.trueWidth / this.cChartDrawer.calcProp.pxToMM) + startCat;
let endVal = (this.cChartDrawer.calcProp.trueHeight / this.cChartDrawer.calcProp.pxToMM) + startVal;

if (lastPoint.catVal < startCat || lastPoint.catVal > endCat) {
lastPoint.catVal = 0;
}

if (lastPoint.valVal < startVal || lastPoint.valVal > endVal) {
lastPoint.valVal = 0;
}

if (coefficients || rSquared) {
const additionalInfo = {
coefficients : coefficients,
Expand Down Expand Up @@ -16312,7 +16337,6 @@ CColorObj.prototype =
const catAxis = this.cChartDrawer._searchChangedAxis(oChart.axId[0]);
const valAxis = this.cChartDrawer._searchChangedAxis(oChart.axId[1]);
const coords = storageElement.getCoords();

// moving average differs much from other trendlines
if (type === AscFormat.TRENDLINE_TYPE_MOVING_AVG) {
const period = attributes.period ? attributes.period : 2;
Expand All @@ -16336,6 +16360,7 @@ CColorObj.prototype =
const cutPoint = valAxis.scaling.logBase ? (Math.log(1000) / Math.log(valAxis.scaling.logBase)) : 1000;
const lineCoords = lineBuilder.drawWithApproximatedBezier(0.01, 1.56, cutPoint);
storageElement.setBezierApproximationResults(lineCoords.mainLine, lineCoords.startPoint);
storageElement.setLineReversed(lineCoords.isReversed);
}
if (this.bAllowDrawByPoints || type === AscFormat.TRENDLINE_TYPE_POLY) {
const minLogVal = storageElement.getMinLogVal();
Expand All @@ -16361,6 +16386,8 @@ CColorObj.prototype =
if (boundary) {
const catAxis = this.cChartDrawer._searchChangedAxis(oChart.axId[0]);
const valAxis = this.cChartDrawer._searchChangedAxis(oChart.axId[1]);
boundary.catMax = Math.ceil(boundary.catMax);
boundary.catMin = Math.floor(boundary.catMin);

if (!boundaries[catAxis.Id]) {
boundaries[catAxis.Id] = {min: null, max: null};
Expand Down Expand Up @@ -16410,7 +16437,7 @@ CColorObj.prototype =
const _valsToPos = function (arr, cChartDrawer) {
const posArr = {xPos: [], yPos: []}
for (let i = 0; i < arr.catVals.length; i++) {
let pos1 = cChartDrawer.getYPosition(arr.catVals[i], catAxis);
let pos1 = cChartDrawer.getYPosition(arr.catVals[i], catAxis, true);
let pos2 = cChartDrawer.getYPosition(arr.valVals[i], valAxis, true);

let catPos = horOrientation ? pos1 : pos2;
Expand Down Expand Up @@ -16452,7 +16479,7 @@ CColorObj.prototype =
}
}

storageElement.setLastPoint(positions.xPos[positions.xPos.length - 1], positions.yPos[positions.yPos.length - 1]);
storageElement.setLastPoint(positions.xPos, positions.yPos);
storageElement.setBezierPath(pathId);
}

Expand All @@ -16467,7 +16494,7 @@ CColorObj.prototype =
for (let i = 0; i < positions.xPos.length; i++) {
path.lnTo(positions.xPos[i] * pathW, positions.yPos[i] * pathH);
}
storageElement.setLastPoint(positions.xPos[positions.xPos.length - 1], positions.yPos[positions.yPos.length - 1]);
storageElement.setLastPoint(positions.xPos, positions.yPos);
storageElement.setPointPath(pathId);
}
},
Expand Down Expand Up @@ -16629,7 +16656,7 @@ CColorObj.prototype =
return true;
};

const mapped = _mapCoordinates();
const mapped = this.continueAdding ? _mapCoordinates() : true;

if (mapped) {

Expand Down Expand Up @@ -16818,7 +16845,6 @@ CColorObj.prototype =
const Y = _createMatrix(valVals, 1, 1, intercept);
const S = _matMul(X_T, Y, pow - y_intercept, 1, catVals.length);
const letiables = _matMul(P_I, S, pow - y_intercept, 1, pow - y_intercept);

return flatten(letiables, y_intercept, intercept);
}
}
Expand Down Expand Up @@ -16855,18 +16881,7 @@ CColorObj.prototype =
},

_dispRSquared: function (catVals, valVals, coefficients, type) {
const mappingStorage = this._obtainMappingStorage(type)
const findYMean = function () {
let sum = 0;
let size = valVals.length;
for (let i = 0; i < size; i++) {
sum += valVals[i]
}
return sum / size;
};

const yMean = findYMean();

const mappingStorage = this._obtainMappingStorage(type);
const predictY = function (xVal) {
let result = mappingStorage.bValForward ? mappingStorage.bValForward(coefficients[coefficients.length - 1]) : coefficients[coefficients.length - 1];
let power = 1;
Expand All @@ -16877,18 +16892,32 @@ CColorObj.prototype =
return result;
};

// R squared = 1 - sumRegression/sumSquared
// sumRegression is ∑(y-yPredicted)^2
// sumSquared is ∑(y-yMean)^2
// R squared = Correlation * Correlation
// Correlation = (N∑xy-∑x∑y) / (sqrt([N∑x^2-(∑x)^2][N∑y^2-(∑y)^2]))

const N = valVals.length;
let sumSquared = 0;
let sumRegression = 0;
for (let i = 0; i < valVals.length; i++) {
let XY = 0;
let X = 0;
let Y = 0;
let XSquared = 0;
let YSquared = 0;
for (let i = 0; i < N; i++) {
const yValPred = predictY(catVals[i]);
sumRegression += Math.pow((valVals[i] - yValPred), 2);
sumSquared += Math.pow((valVals[i] - yMean), 2);
const yVal = valVals[i];
XY += (yVal * yValPred);
X += yVal;
Y += yValPred;
XSquared += (yVal * yVal);
YSquared += (yValPred * yValPred);
}
let divisor = (Math.sqrt((N * XSquared - Math.pow(X, 2)) * (N * YSquared - Math.pow(Y, 2))));
if (divisor != 0) {
const correlation = ((N * XY) - (X * Y)) / divisor;
return correlation * correlation;
}
return 1 - (sumRegression / sumSquared);
return null;
},

draw: function () {
Expand Down Expand Up @@ -16985,7 +17014,7 @@ CColorObj.prototype =
if (!this.isLog && !cutPoint) {
cutPoint = 1000;
}
const results = this._calclApproximatedBezier(error, tailLimit, cutPoint);
const results = this._calcApproximatedBezier(error, tailLimit, cutPoint);
if (this.isLog) {
this._normalize(results.mainLine);
this._normalize(results.startPoint);
Expand Down Expand Up @@ -17023,7 +17052,7 @@ CColorObj.prototype =
// this function can return two lines
// secondaryLine which is just a straight line
// primaryLine which is bezier driven line
_calclApproximatedBezier: function (error, tailLimit, cutPoint) {
_calcApproximatedBezier: function (error, tailLimit, cutPoint) {
// error: the threshold to check the accuracy of the approximated line
// tailLimit: the threshold at which the slope of the point becomes too high leading to its arctangent to become high
// cutPoint: the threshold at which line should be cutted. For lines with a long tails, approximation does not work, therefore they must be cutted
Expand All @@ -17035,26 +17064,37 @@ CColorObj.prototype =
// Step 1 find edges of line
// --------------------------------------

const start = {catVal: null, valVal: null};
let start = {catVal: null, valVal: null};
const end = {catVal: null, valVal: null};
this._lineCoordinate(start, this.catMin, true);
this._lineCoordinate(end, this.catMax, true);

// yValue for log scale can not be <= 0
// therefore use close to 0 val
if (this.isLog) {
const lowerBoundary = (Math.log(0.01) / Math.log(this.isLog));
if (isNaN(start.valVal)) {
this._lineCoordinate(start, lowerBoundary, false);
}
if (isNaN(end.valVal)) {
this._lineCoordinate(start, lowerBoundary, false);
this._lineCoordinate(end, lowerBoundary, false);
}
}

this._checkBoundaries(start);
this._checkBoundaries(end);

//sometimes lines can be reversed, we need to track directions
//start point cant be changed with end point
let isReversed = false;
//start should always be lower than end
if (start.valVal > end.valVal) {
isReversed = true;
const temp = {catVal: end.catVal, valVal: end.valVal};
end.valVal = start.valVal;
end.catVal = start.catVal;
start = temp;
}

//sometimes lines can point in opposite direction, we need to track directions
let direction = true;
const line2Letiables = this._findLine(end.catVal, end.valVal);
let line1Letiables = this._findLine(start.catVal, start.valVal);
Expand All @@ -17065,16 +17105,16 @@ CColorObj.prototype =

// sometimes graph can have huge tails, to detect them use slope of starting and end point of line
// if the slope of the starting point is to high, then cut the line up to certain point (lowestPoint)
if (Math.atan(line2Letiables[1]) < tailLimit && Math.atan(line1Letiables[1]) > tailLimit && start.valVal < lowestPoint) {
if (Math.atan(Math.abs(line2Letiables[1])) < tailLimit && Math.atan(Math.abs(line1Letiables[1])) > tailLimit && start.valVal < lowestPoint) {
this._lineCoordinate(start, lowestPoint, false);
line1Letiables = this._findLine(start.catVal, start.valVal);
}
// if the slope of the ending point is to high, then cut the line into 2/3 of its original size
const startPoint = {catVals: [], valVals: []};
if (Math.atan(line1Letiables[1]) < tailLimit && Math.atan(line2Letiables[1]) > tailLimit) {
if (Math.atan(Math.abs(line1Letiables[1])) < tailLimit && Math.atan(Math.abs(line2Letiables[1])) > tailLimit) {
startPoint.catVals.push(start.catVal);
startPoint.valVals.push(end.catVal);
this._lineCoordinate(start, (end.catVal + start.catVal) / 2, true);
startPoint.valVals.push(start.valVal);
start = this._cutLine(start, end);
line1Letiables = this._findLine(start.catVal, start.valVal);
direction = false;
}
Expand All @@ -17087,7 +17127,7 @@ CColorObj.prototype =
if (line2Letiables[1] === line1Letiables[1]) {
controlPoints.catVals.push(end.catVal);
controlPoints.valVals.push(end.valVal);
return {startPoint: startPoint, mainLine: controlPoints}
return {isReversed: isReversed, startPoint: startPoint, mainLine: controlPoints}
}

// --------------------------------------
Expand All @@ -17113,7 +17153,7 @@ CColorObj.prototype =
const valMin = Math.min(val1, val2);
const predictedErr = this._check(controlPoints, valMin, valMax);
if (predictedErr <= error && predictedErr >= -error) {
return {startPoint: startPoint, mainLine: controlPoints}
return {isReversed: isReversed, startPoint: startPoint, mainLine: controlPoints}
}
//--------------------------------------------
//Step 5 find approximation of cubic bezier;
Expand Down Expand Up @@ -17148,7 +17188,7 @@ CColorObj.prototype =
controlPoints.valVals[1] = storage.cpY;
}

return {startPoint: startPoint, mainLine: controlPoints};
return {isReversed: isReversed, startPoint: startPoint, mainLine: controlPoints};
},

// find catVal and valVal of line at given point
Expand Down Expand Up @@ -17185,6 +17225,30 @@ CColorObj.prototype =
return [b, m];
},

//we need to cut a tail of the line
//up to 1 percent of its Val (Y) length
_cutLine: function (start, end) {
let treshold = 0.05;
let obtainedError = 0;
const maxIterations = 10;
let i = 0;
let startingPoint = {catVal: start.catVal, valVal: start.valVal};
let prev = null;
let newObtainedError = null;
while (obtainedError < treshold && i < maxIterations) {
prev = {catVal: start.catVal, valVal: start.valVal};
this._lineCoordinate(start, (end.catVal + start.catVal) / 2, true);
newObtainedError = 1 - (end.valVal - start.valVal)/ (end.valVal - startingPoint.valVal);
if (newObtainedError >= obtainedError) {
obtainedError = newObtainedError;
} else {
break;
}
i++;
}
return prev ? prev : start;
},

// predict catVal and valVal from bezier line using bernstein polynomials
// find actual valVal at given predicted catVal
// compare both valVals (actual and predicted) on normalized scale
Expand Down Expand Up @@ -17315,13 +17379,21 @@ CColorObj.prototype =
this.boundary = null;
this.minLogVal = null;
this.startBezierVal = null;
this.isReversed = false;
this.lastPoint = {catVal: null, valVal: null};
}

// set calcYVal! calcXVal! calcSlope!
CTrendData.prototype = {
constructor: CTrendData,

isEmpty: function () {
if (this.coords.catVals.length === 0 || this.coords.valVals.length === 0){
return true;
}
return false;
},

getBezierPath: function () {
return this.bezierPath;
},
Expand Down Expand Up @@ -17378,6 +17450,10 @@ CColorObj.prototype =
this.startBezierVal = startingPoint;
},

setLineReversed: function (isReversed) {
this.isReversed = isReversed;
},

addCatVal: function (val) {
this.coords.catVals.push(val);
},
Expand Down Expand Up @@ -17410,9 +17486,9 @@ CColorObj.prototype =
this.boundary = boundary;
},

setLastPoint: function (catVal, valVal) {
this.lastPoint.catVal = catVal;
this.lastPoint.valVal = valVal;
setLastPoint: function (catArr, valArr) {
this.lastPoint.catVal = this.isReversed ? catArr[0] : catArr[catArr.length - 1];
this.lastPoint.valVal = this.isReversed ? valArr[0] : valArr[valArr.length - 1];
},

setBezierPath: function (bezierPath) {
Expand Down

0 comments on commit fa94f73

Please sign in to comment.