pptx-angular-viewer 1.1.14 → 1.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -3274,6 +3274,271 @@ function resolveCategoryLabels(chartData) {
|
|
|
3274
3274
|
return Array.from({ length: longest }, (_, i) => String(i + 1));
|
|
3275
3275
|
}
|
|
3276
3276
|
|
|
3277
|
+
/**
|
|
3278
|
+
* Map a (possibly fractional / extrapolated) category index to an X pixel.
|
|
3279
|
+
* `mode === 'bar'` centres on category slots; `'line'` anchors at points.
|
|
3280
|
+
* Mirrors the React `xToPixel`.
|
|
3281
|
+
*/
|
|
3282
|
+
function xToPixel(xVal, catCount, layout, mode) {
|
|
3283
|
+
if (mode === 'bar') {
|
|
3284
|
+
const slotWidth = layout.plotWidth / Math.max(catCount, 1);
|
|
3285
|
+
return layout.plotLeft + slotWidth * xVal + slotWidth / 2;
|
|
3286
|
+
}
|
|
3287
|
+
const maxIdx = Math.max(catCount - 1, 1);
|
|
3288
|
+
return layout.plotLeft + (xVal / maxIdx) * layout.plotWidth;
|
|
3289
|
+
}
|
|
3290
|
+
/** Ordinary least-squares linear regression of `yVals` on `xVals`. */
|
|
3291
|
+
function computeLinearRegression(xVals, yVals) {
|
|
3292
|
+
const n = xVals.length;
|
|
3293
|
+
if (n < 2) {
|
|
3294
|
+
return { slope: 0, intercept: 0, rSquared: 0 };
|
|
3295
|
+
}
|
|
3296
|
+
let sumX = 0;
|
|
3297
|
+
let sumY = 0;
|
|
3298
|
+
let sumXY = 0;
|
|
3299
|
+
let sumXX = 0;
|
|
3300
|
+
for (let i = 0; i < n; i++) {
|
|
3301
|
+
sumX += xVals[i];
|
|
3302
|
+
sumY += yVals[i];
|
|
3303
|
+
sumXY += xVals[i] * yVals[i];
|
|
3304
|
+
sumXX += xVals[i] * xVals[i];
|
|
3305
|
+
}
|
|
3306
|
+
const denom = n * sumXX - sumX * sumX;
|
|
3307
|
+
if (Math.abs(denom) < 1e-12) {
|
|
3308
|
+
return { slope: 0, intercept: sumY / n, rSquared: 0 };
|
|
3309
|
+
}
|
|
3310
|
+
const slope = (n * sumXY - sumX * sumY) / denom;
|
|
3311
|
+
const intercept = (sumY - slope * sumX) / n;
|
|
3312
|
+
const ssRes = yVals.reduce((s, y, i) => s + (y - (slope * xVals[i] + intercept)) ** 2, 0);
|
|
3313
|
+
const meanY = sumY / n;
|
|
3314
|
+
const ssTot = yVals.reduce((s, y) => s + (y - meanY) ** 2, 0);
|
|
3315
|
+
const rSquared = ssTot > 0 ? 1 - ssRes / ssTot : 0;
|
|
3316
|
+
return { slope, intercept, rSquared };
|
|
3317
|
+
}
|
|
3318
|
+
/** Fit polynomial coefficients (ascending order) via Gaussian elimination. */
|
|
3319
|
+
function fitPolynomial(xVals, yVals, order) {
|
|
3320
|
+
const n = xVals.length;
|
|
3321
|
+
const m = order + 1;
|
|
3322
|
+
const matrix = Array.from({ length: m }, () => Array(m + 1).fill(0));
|
|
3323
|
+
for (let i = 0; i < m; i++) {
|
|
3324
|
+
for (let j = 0; j < m; j++) {
|
|
3325
|
+
let sum = 0;
|
|
3326
|
+
for (let k = 0; k < n; k++) {
|
|
3327
|
+
sum += xVals[k] ** (i + j);
|
|
3328
|
+
}
|
|
3329
|
+
matrix[i][j] = sum;
|
|
3330
|
+
}
|
|
3331
|
+
let sum = 0;
|
|
3332
|
+
for (let k = 0; k < n; k++) {
|
|
3333
|
+
sum += yVals[k] * xVals[k] ** i;
|
|
3334
|
+
}
|
|
3335
|
+
matrix[i][m] = sum;
|
|
3336
|
+
}
|
|
3337
|
+
for (let i = 0; i < m; i++) {
|
|
3338
|
+
let maxRow = i;
|
|
3339
|
+
for (let k = i + 1; k < m; k++) {
|
|
3340
|
+
if (Math.abs(matrix[k][i]) > Math.abs(matrix[maxRow][i])) {
|
|
3341
|
+
maxRow = k;
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
[matrix[i], matrix[maxRow]] = [matrix[maxRow], matrix[i]];
|
|
3345
|
+
const pivot = matrix[i][i];
|
|
3346
|
+
if (Math.abs(pivot) < 1e-12) {
|
|
3347
|
+
continue;
|
|
3348
|
+
}
|
|
3349
|
+
for (let j = i; j <= m; j++) {
|
|
3350
|
+
matrix[i][j] /= pivot;
|
|
3351
|
+
}
|
|
3352
|
+
for (let k = 0; k < m; k++) {
|
|
3353
|
+
if (k === i) {
|
|
3354
|
+
continue;
|
|
3355
|
+
}
|
|
3356
|
+
const factor = matrix[k][i];
|
|
3357
|
+
for (let j = i; j <= m; j++) {
|
|
3358
|
+
matrix[k][j] -= factor * matrix[i][j];
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
return matrix.map((row) => row[m]);
|
|
3363
|
+
}
|
|
3364
|
+
/** R² of an arbitrary fit function against the data. */
|
|
3365
|
+
function computeRSquared(xVals, yVals, evalFn) {
|
|
3366
|
+
const n = xVals.length;
|
|
3367
|
+
const meanY = yVals.reduce((s, y) => s + y, 0) / n;
|
|
3368
|
+
let ssRes = 0;
|
|
3369
|
+
let ssTot = 0;
|
|
3370
|
+
for (let i = 0; i < n; i++) {
|
|
3371
|
+
ssRes += (yVals[i] - evalFn(xVals[i])) ** 2;
|
|
3372
|
+
ssTot += (yVals[i] - meanY) ** 2;
|
|
3373
|
+
}
|
|
3374
|
+
return ssTot > 0 ? 1 - ssRes / ssTot : 0;
|
|
3375
|
+
}
|
|
3376
|
+
/**
|
|
3377
|
+
* Compute the polyline points (and equation / R²) for one trendline over a
|
|
3378
|
+
* series' values. Returns an empty point list when the type is unsupported or
|
|
3379
|
+
* the data is too sparse to fit. Mirrors the React `computeTrendlinePoints`.
|
|
3380
|
+
*/
|
|
3381
|
+
function computeTrendlinePoints(trendline, values, catCount, layout, range, mode) {
|
|
3382
|
+
const n = values.length;
|
|
3383
|
+
if (n < 2) {
|
|
3384
|
+
return { points: [], equation: '', rSquared: 0 };
|
|
3385
|
+
}
|
|
3386
|
+
const xVals = values.map((_v, i) => i);
|
|
3387
|
+
const yVals = values;
|
|
3388
|
+
const forward = trendline.forward ?? 0;
|
|
3389
|
+
const backward = trendline.backward ?? 0;
|
|
3390
|
+
const startX = -backward;
|
|
3391
|
+
const endX = n - 1 + forward;
|
|
3392
|
+
const steps = Math.max(Math.ceil((endX - startX) * 4), 20);
|
|
3393
|
+
let evalFn;
|
|
3394
|
+
let equation = '';
|
|
3395
|
+
let rSquared = 0;
|
|
3396
|
+
switch (trendline.trendlineType) {
|
|
3397
|
+
case 'linear': {
|
|
3398
|
+
const reg = computeLinearRegression(xVals, yVals);
|
|
3399
|
+
const intercept = trendline.intercept;
|
|
3400
|
+
const slope = intercept !== undefined
|
|
3401
|
+
? yVals.reduce((s, y, i) => s + (y - intercept) * xVals[i], 0) /
|
|
3402
|
+
xVals.reduce((s, x) => s + x * x, 0)
|
|
3403
|
+
: reg.slope;
|
|
3404
|
+
const b = intercept ?? reg.intercept;
|
|
3405
|
+
evalFn = (x) => slope * x + b;
|
|
3406
|
+
equation = `y = ${slope.toFixed(2)}x + ${b.toFixed(2)}`;
|
|
3407
|
+
rSquared = reg.rSquared;
|
|
3408
|
+
break;
|
|
3409
|
+
}
|
|
3410
|
+
case 'exponential': {
|
|
3411
|
+
const logY = yVals.filter((y) => y > 0).map((y) => Math.log(y));
|
|
3412
|
+
const filteredX = xVals.filter((_x, i) => yVals[i] > 0);
|
|
3413
|
+
if (logY.length < 2) {
|
|
3414
|
+
return { points: [], equation: '', rSquared: 0 };
|
|
3415
|
+
}
|
|
3416
|
+
const reg = computeLinearRegression(filteredX, logY);
|
|
3417
|
+
const a = Math.exp(reg.intercept);
|
|
3418
|
+
const b = reg.slope;
|
|
3419
|
+
evalFn = (x) => a * Math.exp(b * x);
|
|
3420
|
+
equation = `y = ${a.toFixed(2)}e^(${b.toFixed(2)}x)`;
|
|
3421
|
+
rSquared = reg.rSquared;
|
|
3422
|
+
break;
|
|
3423
|
+
}
|
|
3424
|
+
case 'logarithmic': {
|
|
3425
|
+
const lnX = xVals.filter((x) => x > 0).map((x) => Math.log(x));
|
|
3426
|
+
const filteredY = yVals.filter((_y, i) => xVals[i] > 0);
|
|
3427
|
+
if (lnX.length < 2) {
|
|
3428
|
+
return { points: [], equation: '', rSquared: 0 };
|
|
3429
|
+
}
|
|
3430
|
+
const reg = computeLinearRegression(lnX, filteredY);
|
|
3431
|
+
evalFn = (x) => (x > 0 ? reg.slope * Math.log(x) + reg.intercept : 0);
|
|
3432
|
+
equation = `y = ${reg.slope.toFixed(2)}ln(x) + ${reg.intercept.toFixed(2)}`;
|
|
3433
|
+
rSquared = reg.rSquared;
|
|
3434
|
+
break;
|
|
3435
|
+
}
|
|
3436
|
+
case 'power': {
|
|
3437
|
+
const logX = xVals.filter((x, i) => x > 0 && yVals[i] > 0).map((x) => Math.log(x));
|
|
3438
|
+
const logY = yVals.filter((y, i) => y > 0 && xVals[i] > 0).map((y) => Math.log(y));
|
|
3439
|
+
if (logX.length < 2) {
|
|
3440
|
+
return { points: [], equation: '', rSquared: 0 };
|
|
3441
|
+
}
|
|
3442
|
+
const reg = computeLinearRegression(logX, logY);
|
|
3443
|
+
const a = Math.exp(reg.intercept);
|
|
3444
|
+
evalFn = (x) => (x > 0 ? a * x ** reg.slope : 0);
|
|
3445
|
+
equation = `y = ${a.toFixed(2)}x^${reg.slope.toFixed(2)}`;
|
|
3446
|
+
rSquared = reg.rSquared;
|
|
3447
|
+
break;
|
|
3448
|
+
}
|
|
3449
|
+
case 'polynomial': {
|
|
3450
|
+
const order = Math.min(trendline.order ?? 2, 6);
|
|
3451
|
+
const coeffs = fitPolynomial(xVals, yVals, order);
|
|
3452
|
+
evalFn = (x) => coeffs.reduce((s, c, i) => s + c * x ** i, 0);
|
|
3453
|
+
equation = coeffs.map((c, i) => `${c.toFixed(2)}x^${i}`).join(' + ');
|
|
3454
|
+
rSquared = computeRSquared(xVals, yVals, evalFn);
|
|
3455
|
+
break;
|
|
3456
|
+
}
|
|
3457
|
+
case 'movingAvg': {
|
|
3458
|
+
const period = trendline.period ?? 2;
|
|
3459
|
+
const maPoints = [];
|
|
3460
|
+
for (let i = period - 1; i < n; i++) {
|
|
3461
|
+
let sum = 0;
|
|
3462
|
+
for (let j = i - period + 1; j <= i; j++) {
|
|
3463
|
+
sum += yVals[j];
|
|
3464
|
+
}
|
|
3465
|
+
const avgVal = sum / period;
|
|
3466
|
+
const px = xToPixel(i, catCount, layout, mode);
|
|
3467
|
+
const py = valueToYLocal(avgVal, range, layout.plotTop, layout.plotBottom);
|
|
3468
|
+
maPoints.push({ x: px, y: py });
|
|
3469
|
+
}
|
|
3470
|
+
return {
|
|
3471
|
+
points: maPoints,
|
|
3472
|
+
equation: `${period}-period moving average`,
|
|
3473
|
+
rSquared: 0,
|
|
3474
|
+
};
|
|
3475
|
+
}
|
|
3476
|
+
default:
|
|
3477
|
+
return { points: [], equation: '', rSquared: 0 };
|
|
3478
|
+
}
|
|
3479
|
+
const points = [];
|
|
3480
|
+
for (let step = 0; step <= steps; step++) {
|
|
3481
|
+
const xVal = startX + ((endX - startX) * step) / steps;
|
|
3482
|
+
const yVal = evalFn(xVal);
|
|
3483
|
+
if (!Number.isFinite(yVal)) {
|
|
3484
|
+
continue;
|
|
3485
|
+
}
|
|
3486
|
+
const px = xToPixel(xVal, catCount, layout, mode);
|
|
3487
|
+
const py = valueToYLocal(yVal, range, layout.plotTop, layout.plotBottom);
|
|
3488
|
+
points.push({ x: px, y: py });
|
|
3489
|
+
}
|
|
3490
|
+
return { points, equation, rSquared };
|
|
3491
|
+
}
|
|
3492
|
+
/**
|
|
3493
|
+
* Map a value to a Y pixel. Kept local (rather than importing `valueToY`) so
|
|
3494
|
+
* the trendline maths is independent of any future change to the linear
|
|
3495
|
+
* mapping used by the axis renderer — they happen to coincide today.
|
|
3496
|
+
*/
|
|
3497
|
+
function valueToYLocal(val, range, topY, bottomY) {
|
|
3498
|
+
const usable = bottomY - topY;
|
|
3499
|
+
return bottomY - ((val - range.min) / range.span) * usable;
|
|
3500
|
+
}
|
|
3501
|
+
/**
|
|
3502
|
+
* Build the renderable trendlines for every series in a chart. Returns an
|
|
3503
|
+
* empty array when no series declares any trendline (so a binding can skip
|
|
3504
|
+
* the overlay entirely).
|
|
3505
|
+
*
|
|
3506
|
+
* @param mode 'bar' for bar/column/stacked plots, 'line' for line/area plots.
|
|
3507
|
+
*/
|
|
3508
|
+
function computeChartTrendlines(chartData, layout, range, mode, styleId, colorPalette) {
|
|
3509
|
+
const catCount = Math.max(chartData.categories.length, 1);
|
|
3510
|
+
const out = [];
|
|
3511
|
+
chartData.series.forEach((series, si) => {
|
|
3512
|
+
if (!series.trendlines || series.trendlines.length === 0) {
|
|
3513
|
+
return;
|
|
3514
|
+
}
|
|
3515
|
+
series.trendlines.forEach((tl) => {
|
|
3516
|
+
const { points, equation, rSquared } = computeTrendlinePoints(tl, series.values, catCount, layout, range, mode);
|
|
3517
|
+
if (points.length < 2) {
|
|
3518
|
+
return;
|
|
3519
|
+
}
|
|
3520
|
+
const pathData = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`).join(' ');
|
|
3521
|
+
const color = tl.color || seriesColor$1(series, si, styleId, colorPalette);
|
|
3522
|
+
const labelParts = [];
|
|
3523
|
+
if (tl.displayEq && equation) {
|
|
3524
|
+
labelParts.push(equation);
|
|
3525
|
+
}
|
|
3526
|
+
if (tl.displayRSq) {
|
|
3527
|
+
labelParts.push(`R² = ${rSquared.toFixed(4)}`);
|
|
3528
|
+
}
|
|
3529
|
+
const last = points[points.length - 1];
|
|
3530
|
+
out.push({
|
|
3531
|
+
pathData,
|
|
3532
|
+
color,
|
|
3533
|
+
label: labelParts.length > 0 ? labelParts.join(' ') : undefined,
|
|
3534
|
+
labelX: labelParts.length > 0 ? last.x : undefined,
|
|
3535
|
+
labelY: labelParts.length > 0 ? last.y - 6 : undefined,
|
|
3536
|
+
});
|
|
3537
|
+
});
|
|
3538
|
+
});
|
|
3539
|
+
return out;
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3277
3542
|
/**
|
|
3278
3543
|
* `animation-css` — pure mapping from a core animation preset to CSS.
|
|
3279
3544
|
*
|
|
@@ -5232,7 +5497,8 @@ function snapAngle(angleDeg, step = 15, tolerance = step / 2) {
|
|
|
5232
5497
|
* - fills: `fill-style` (image/gradient/pattern/solid → CSS).
|
|
5233
5498
|
* - effects: `visual-effects` (shadow/glow/reflection/DAG), `image-effects`.
|
|
5234
5499
|
* - text: `text-warp` (WordArt paths), `omml-to-mathml` (equations).
|
|
5235
|
-
* - charts: `chart-helpers` (layout/palette/axis math)
|
|
5500
|
+
* - charts: `chart-helpers` (layout/palette/axis math), `chart-trendlines`
|
|
5501
|
+
* (regression overlays).
|
|
5236
5502
|
* - animation: `animation-css` (preset → CSS keyframes).
|
|
5237
5503
|
* - 3d: `visual-3d` (scene3d/shape3d → CSS transform/shadow pieces).
|
|
5238
5504
|
* - tables: `table-style` (cell style + banding → CSS).
|