cx 26.7.6 → 26.8.0
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.
- package/build/charts/LineGraph.d.ts +8 -16
- package/build/charts/LineGraph.d.ts.map +1 -1
- package/build/charts/LineGraph.js +76 -69
- package/build/charts/axis/NumericAxis.d.ts.map +1 -1
- package/build/charts/axis/NumericAxis.js +1 -254
- package/build/charts/axis/NumericScale.d.ts +59 -0
- package/build/charts/axis/NumericScale.d.ts.map +1 -0
- package/build/charts/axis/NumericScale.js +254 -0
- package/build/widgets/grid/Grid.d.ts +9 -0
- package/build/widgets/grid/Grid.d.ts.map +1 -1
- package/build/widgets/grid/Grid.js +17 -4
- package/dist/charts.js +156 -165
- package/dist/manifest.js +703 -700
- package/dist/widgets.js +14 -3
- package/package.json +1 -1
- package/src/charts/LineGraph.tsx +80 -98
- package/src/charts/axis/NumericAxis.spec.ts +40 -0
- package/src/charts/axis/NumericAxis.tsx +1 -290
- package/src/charts/axis/NumericScale.ts +290 -0
- package/src/widgets/grid/Grid.sorting.spec.tsx +188 -0
- package/src/widgets/grid/Grid.tsx +28 -5
package/dist/widgets.js
CHANGED
|
@@ -17901,7 +17901,12 @@ class Grid extends ContainerBase$1 {
|
|
|
17901
17901
|
let line = instance.row[`line${l}`];
|
|
17902
17902
|
let sortColumn = line && line.columns && line.columns.find((c) => (c.sortField || c.field) == sortField);
|
|
17903
17903
|
if (sortColumn) {
|
|
17904
|
-
|
|
17904
|
+
// precedence: sortValue > sortField > value > field
|
|
17905
|
+
data.sorters[0].value = isDefined(sortColumn.sortValue)
|
|
17906
|
+
? sortColumn.sortValue
|
|
17907
|
+
: sortColumn.sortField
|
|
17908
|
+
? undefined
|
|
17909
|
+
: sortColumn.value;
|
|
17905
17910
|
data.sorters[0].comparer = sortColumn.comparer;
|
|
17906
17911
|
data.sorters[0].sortOptions = sortColumn.sortOptions;
|
|
17907
17912
|
break;
|
|
@@ -18209,7 +18214,11 @@ class Grid extends ContainerBase$1 {
|
|
|
18209
18214
|
mods.push("sortable");
|
|
18210
18215
|
let sorter = data.sorters && data.sorters[0];
|
|
18211
18216
|
let sortColumnField = hdwidget.sortField || hdwidget.field;
|
|
18212
|
-
let sortColumnValue = isDefined(hdwidget.sortValue)
|
|
18217
|
+
let sortColumnValue = isDefined(hdwidget.sortValue)
|
|
18218
|
+
? hdwidget.sortValue
|
|
18219
|
+
: hdwidget.sortField
|
|
18220
|
+
? undefined
|
|
18221
|
+
: hdwidget.value;
|
|
18213
18222
|
// a sort is identified by its (field, value selector) pair, so columns
|
|
18214
18223
|
// sorting by the same field through different value selectors don't both match
|
|
18215
18224
|
let sorted =
|
|
@@ -18386,7 +18395,9 @@ class Grid extends ContainerBase$1 {
|
|
|
18386
18395
|
let { data } = instance;
|
|
18387
18396
|
let header = column.components[`header${headerLine + 1}`];
|
|
18388
18397
|
let field = column.sortField || column.field;
|
|
18389
|
-
|
|
18398
|
+
// precedence: sortValue > sortField > value > field; the comparer prefers value
|
|
18399
|
+
// over field, so value must not be attached when an explicit sortField is set
|
|
18400
|
+
let value = isDefined(column.sortValue) ? column.sortValue : column.sortField ? undefined : column.value;
|
|
18390
18401
|
let comparer = column.comparer;
|
|
18391
18402
|
let sortOptions = column.sortOptions;
|
|
18392
18403
|
if (header && header.allowSorting && column.sortable && (field || isDefined(value))) {
|
package/package.json
CHANGED
package/src/charts/LineGraph.tsx
CHANGED
|
@@ -42,10 +42,14 @@ export interface LineGraphConfig extends WidgetConfig {
|
|
|
42
42
|
/** Indicate that values should be stacked on top of the other values. Default value is `false`. */
|
|
43
43
|
stacked?: BooleanProp;
|
|
44
44
|
|
|
45
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* Set to `true` to enable smooth (curved) line rendering. Uses monotone cubic
|
|
47
|
+
* interpolation which never overshoots the actual data range, i.e. the curve
|
|
48
|
+
* stays within the vertical bounds of the data.
|
|
49
|
+
*/
|
|
46
50
|
smooth?: BooleanProp;
|
|
47
51
|
|
|
48
|
-
/**
|
|
52
|
+
/** @deprecated Smoothing is based on monotone cubic interpolation and its curvature is not configurable. This property is ignored. */
|
|
49
53
|
smoothingRatio?: NumberProp;
|
|
50
54
|
|
|
51
55
|
/** Name of the horizontal axis. Default value is `x`. */
|
|
@@ -128,7 +132,6 @@ export class LineGraph extends Widget {
|
|
|
128
132
|
declare legendShape: string;
|
|
129
133
|
declare stack: string;
|
|
130
134
|
declare smooth: boolean;
|
|
131
|
-
declare smoothingRatio: number;
|
|
132
135
|
|
|
133
136
|
constructor(config: LineGraphConfig) {
|
|
134
137
|
super(config);
|
|
@@ -159,7 +162,6 @@ export class LineGraph extends Widget {
|
|
|
159
162
|
stack: undefined,
|
|
160
163
|
stacked: undefined,
|
|
161
164
|
smooth: undefined,
|
|
162
|
-
smoothingRatio: undefined,
|
|
163
165
|
});
|
|
164
166
|
}
|
|
165
167
|
|
|
@@ -168,11 +170,6 @@ export class LineGraph extends Widget {
|
|
|
168
170
|
|
|
169
171
|
if (data.name && !data.colorName) data.colorName = data.name;
|
|
170
172
|
|
|
171
|
-
if (data.smooth && data.smoothingRatio != null) {
|
|
172
|
-
if (data.smoothingRatio < 0) data.smoothingRatio = 0;
|
|
173
|
-
if (data.smoothingRatio > 0.4) data.smoothingRatio = 0.4;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
173
|
super.prepareData(context, instance);
|
|
177
174
|
}
|
|
178
175
|
|
|
@@ -299,19 +296,17 @@ export class LineGraph extends Widget {
|
|
|
299
296
|
};
|
|
300
297
|
|
|
301
298
|
let line: React.ReactNode, area: React.ReactNode;
|
|
302
|
-
const r = data.smoothingRatio;
|
|
303
299
|
|
|
304
300
|
let linePath = "";
|
|
305
301
|
if (data.line) {
|
|
306
302
|
lineSpans.forEach((span) => {
|
|
307
|
-
span.
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
});
|
|
303
|
+
if (span.length == 0) return;
|
|
304
|
+
linePath += `M ${span[0].x} ${span[0].y}`;
|
|
305
|
+
if (data.smooth && span.length >= 2) linePath += this.getMonotoneSpanPath(span, "y");
|
|
306
|
+
else
|
|
307
|
+
span.forEach((p, i) => {
|
|
308
|
+
if (i > 0) linePath += `L ${p.x} ${p.y}`;
|
|
309
|
+
});
|
|
315
310
|
});
|
|
316
311
|
|
|
317
312
|
line = (
|
|
@@ -326,34 +321,20 @@ export class LineGraph extends Widget {
|
|
|
326
321
|
if (data.area) {
|
|
327
322
|
let areaPath = "";
|
|
328
323
|
lineSpans.forEach((span) => {
|
|
329
|
-
|
|
330
|
-
span.
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
closePath = `L ${p.x} ${p.y0}` + closePath;
|
|
344
|
-
} else {
|
|
345
|
-
segment = this.getCurvedPathSegment(p, span, i - 1, i - 2, i - 1, i + 1, r, "y");
|
|
346
|
-
|
|
347
|
-
// closing point
|
|
348
|
-
if (i < span.length - 1)
|
|
349
|
-
closePath = this.getCurvedPathSegment(p, span, i + 1, i + 2, i + 1, i - 1, r, "y0") + closePath;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
areaPath += segment;
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
areaPath += `L ${span[span.length - 1].x} ${span[span.length - 1].y0}`;
|
|
356
|
-
areaPath += closePath;
|
|
324
|
+
if (span.length == 0) return;
|
|
325
|
+
let last = span[span.length - 1];
|
|
326
|
+
areaPath += `M ${span[0].x} ${span[0].y}`;
|
|
327
|
+
if (data.smooth && span.length >= 2) {
|
|
328
|
+
areaPath += this.getMonotoneSpanPath(span, "y");
|
|
329
|
+
areaPath += `L ${last.x} ${last.y0}`;
|
|
330
|
+
areaPath += this.getMonotoneSpanPath(span, "y0", true);
|
|
331
|
+
} else {
|
|
332
|
+
span.forEach((p, i) => {
|
|
333
|
+
if (i > 0) areaPath += `L ${p.x} ${p.y}`;
|
|
334
|
+
});
|
|
335
|
+
areaPath += `L ${last.x} ${last.y0}`;
|
|
336
|
+
for (let i = span.length - 2; i >= 0; i--) areaPath += `L ${span[i].x} ${span[i].y0}`;
|
|
337
|
+
}
|
|
357
338
|
areaPath += "Z";
|
|
358
339
|
});
|
|
359
340
|
|
|
@@ -374,61 +355,63 @@ export class LineGraph extends Widget {
|
|
|
374
355
|
);
|
|
375
356
|
}
|
|
376
357
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
j2: number,
|
|
384
|
-
r: number,
|
|
385
|
-
yField: "y" | "y0" = "y",
|
|
386
|
-
): string {
|
|
387
|
-
const [sx, sy] = this.getControlPoint({ cp: points[i1], pp: points[i2], r, np: p, yField });
|
|
388
|
-
const [ex, ey] = this.getControlPoint({ cp: p, pp: points[j1], np: points[j2], r, reverse: true, yField });
|
|
389
|
-
|
|
390
|
-
return `C ${sx} ${sy}, ${ex} ${ey}, ${p.x} ${p[yField]}`;
|
|
391
|
-
}
|
|
358
|
+
// Fritsch-Carlson monotone cubic interpolation. Tangents are limited so the
|
|
359
|
+
// curve between two points never leaves their vertical range (no overshoot).
|
|
360
|
+
getMonotoneTangents(span: LinePoint[], yField: "y" | "y0"): number[] {
|
|
361
|
+
const n = span.length;
|
|
362
|
+
const m: number[] = new Array(n).fill(0);
|
|
363
|
+
if (n < 2) return m;
|
|
392
364
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
r,
|
|
398
|
-
reverse,
|
|
399
|
-
yField = "y",
|
|
400
|
-
}: {
|
|
401
|
-
cp: LinePoint;
|
|
402
|
-
pp: LinePoint | undefined;
|
|
403
|
-
np: LinePoint | undefined;
|
|
404
|
-
r: number;
|
|
405
|
-
reverse?: boolean;
|
|
406
|
-
yField?: "y" | "y0";
|
|
407
|
-
}): [number, number] {
|
|
408
|
-
// When 'current' is the first or last point of the array 'previous' or 'next' don't exist. Replace with 'current'.
|
|
409
|
-
const p = pp || cp;
|
|
410
|
-
const n = np || cp;
|
|
411
|
-
|
|
412
|
-
// Properties of the opposed-line
|
|
413
|
-
let { angle, length } = this.getLineInfo(p.x, p[yField], n.x, n[yField]);
|
|
414
|
-
// If it is end-control-point, add PI to the angle to go backward
|
|
415
|
-
angle = angle + (reverse ? Math.PI : 0);
|
|
416
|
-
length = length * r;
|
|
417
|
-
// The control point position is relative to the current point
|
|
418
|
-
const x = cp.x + Math.cos(angle) * length;
|
|
419
|
-
const y = cp[yField] + Math.sin(angle) * length;
|
|
420
|
-
return [x, y];
|
|
421
|
-
}
|
|
365
|
+
const secant = (i: number): number => {
|
|
366
|
+
const h = span[i + 1].x - span[i].x;
|
|
367
|
+
return h != 0 ? (span[i + 1][yField] - span[i][yField]) / h : 0;
|
|
368
|
+
};
|
|
422
369
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
370
|
+
if (n == 2) {
|
|
371
|
+
m[0] = m[1] = secant(0);
|
|
372
|
+
return m;
|
|
373
|
+
}
|
|
426
374
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
375
|
+
for (let i = 1; i < n - 1; i++) {
|
|
376
|
+
const h0 = span[i].x - span[i - 1].x;
|
|
377
|
+
const h1 = span[i + 1].x - span[i].x;
|
|
378
|
+
const s0 = secant(i - 1);
|
|
379
|
+
const s1 = secant(i);
|
|
380
|
+
const p = (s0 * h1 + s1 * h0) / (h0 + h1);
|
|
381
|
+
m[i] = (Math.sign(s0) + Math.sign(s1)) * Math.min(Math.abs(s0), Math.abs(s1), 0.5 * Math.abs(p)) || 0;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const hFirst = span[1].x - span[0].x;
|
|
385
|
+
m[0] = hFirst != 0 ? (3 * secant(0) - m[1]) / 2 : m[1];
|
|
386
|
+
|
|
387
|
+
const hLast = span[n - 1].x - span[n - 2].x;
|
|
388
|
+
m[n - 1] = hLast != 0 ? (3 * secant(n - 2) - m[n - 2]) / 2 : m[n - 2];
|
|
389
|
+
|
|
390
|
+
return m;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Emits cubic bezier segments for the whole span using monotone tangents.
|
|
394
|
+
// Assumes the path cursor is at the first (or last, when reversed) span point.
|
|
395
|
+
getMonotoneSpanPath(span: LinePoint[], yField: "y" | "y0", reverse?: boolean): string {
|
|
396
|
+
const m = this.getMonotoneTangents(span, yField);
|
|
397
|
+
let path = "";
|
|
398
|
+
if (!reverse)
|
|
399
|
+
for (let i = 1; i < span.length; i++) {
|
|
400
|
+
const p0 = span[i - 1];
|
|
401
|
+
const p1 = span[i];
|
|
402
|
+
const dx = (p1.x - p0.x) / 3;
|
|
403
|
+
path += `C ${p0.x + dx} ${p0[yField] + dx * m[i - 1]}, ${p1.x - dx} ${p1[yField] - dx * m[i]}, ${p1.x} ${p1[yField]}`;
|
|
404
|
+
}
|
|
405
|
+
else
|
|
406
|
+
for (let i = span.length - 1; i > 0; i--) {
|
|
407
|
+
const p0 = span[i - 1];
|
|
408
|
+
const p1 = span[i];
|
|
409
|
+
const dx = (p1.x - p0.x) / 3;
|
|
410
|
+
path += `C ${p1.x - dx} ${p1[yField] - dx * m[i]}, ${p0.x + dx} ${p0[yField] + dx * m[i - 1]}, ${p0.x} ${p0[yField]}`;
|
|
411
|
+
}
|
|
412
|
+
return path;
|
|
431
413
|
}
|
|
414
|
+
|
|
432
415
|
}
|
|
433
416
|
|
|
434
417
|
LineGraph.prototype.xAxis = "x";
|
|
@@ -449,7 +432,6 @@ LineGraph.prototype.stack = "stack";
|
|
|
449
432
|
LineGraph.prototype.hiddenBase = false;
|
|
450
433
|
|
|
451
434
|
LineGraph.prototype.smooth = false;
|
|
452
|
-
LineGraph.prototype.smoothingRatio = 0.05;
|
|
453
435
|
LineGraph.prototype.styled = true;
|
|
454
436
|
|
|
455
437
|
Widget.alias("line-graph", LineGraph);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import assert from "assert";
|
|
2
|
+
import { NumericScale } from "./NumericScale";
|
|
3
|
+
|
|
4
|
+
// Build a scale with a value range of [0, 100] mapped onto a wider pixel range
|
|
5
|
+
// [0, 500] (factor ~5), so a value's pixel differs substantially from the value.
|
|
6
|
+
function makeScale() {
|
|
7
|
+
const scale = new NumericScale();
|
|
8
|
+
scale.reset(0, 100, 1, [[1, 2, 5, 10]], 25, 0, 40, 0, false, false, 0, 0);
|
|
9
|
+
scale.measure(0, 500);
|
|
10
|
+
return scale;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("NumericScale.trackValue", function () {
|
|
14
|
+
it("maps a pixel back to its value when unconstrained", function () {
|
|
15
|
+
const scale = makeScale();
|
|
16
|
+
const px = scale.map(50); // pixel for the mid-range value 50 (~250)
|
|
17
|
+
assert.ok(Math.abs(scale.trackValue(px, 0, false) - 50) < 1e-6);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Regression guard: `constrain = true` must clamp the computed value, not the
|
|
21
|
+
// raw pixel coordinate. The original bug was:
|
|
22
|
+
// if (constrain) value = this.constrainValue(v); // v is the pixel
|
|
23
|
+
// which, because the pixel range is wider than the value range, wrongly
|
|
24
|
+
// clamped an in-range value whose pixel landed outside [min, max]. It should
|
|
25
|
+
// clamp `value`, like TimeScale.trackValue does.
|
|
26
|
+
it("constrains the returned value, not the pixel", function () {
|
|
27
|
+
const scale = makeScale();
|
|
28
|
+
const value = 50; // squarely inside [0, 100]
|
|
29
|
+
const px = scale.map(value); // ~250 px, i.e. outside [0, 100]
|
|
30
|
+
|
|
31
|
+
const constrained = scale.trackValue(px, 0, true);
|
|
32
|
+
|
|
33
|
+
// 50 is already inside the axis range, so constraining must leave it be.
|
|
34
|
+
assert.ok(
|
|
35
|
+
Math.abs(constrained - 50) < 1e-6,
|
|
36
|
+
`expected trackValue(px, 0, true) to constrain the value to 50, but got ${constrained} ` +
|
|
37
|
+
`(the pixel ${px} was clamped instead of the value)`,
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import { Axis, AxisConfig, AxisInstance } from "./Axis";
|
|
4
4
|
import { VDOM } from "../../ui/Widget";
|
|
5
|
-
import {
|
|
5
|
+
import { NumericScale } from "./NumericScale";
|
|
6
6
|
import { Format } from "../../util/Format";
|
|
7
|
-
import { isNumber } from "../../util/isNumber";
|
|
8
7
|
import { RenderingContext } from "../../ui/RenderingContext";
|
|
9
8
|
import { NumberProp, BooleanProp, StringProp } from "../../ui/Prop";
|
|
10
|
-
import { Console } from "../../util/Console";
|
|
11
9
|
|
|
12
10
|
export interface NumericAxisConfig extends AxisConfig {
|
|
13
11
|
/** Minimum value. */
|
|
@@ -148,290 +146,3 @@ NumericAxis.prototype.minLabelTickSize = 0;
|
|
|
148
146
|
NumericAxis.prototype.minTickStep = 0;
|
|
149
147
|
|
|
150
148
|
Axis.alias("numeric", NumericAxis);
|
|
151
|
-
|
|
152
|
-
class NumericScale {
|
|
153
|
-
min: number;
|
|
154
|
-
max: number;
|
|
155
|
-
snapToTicks: number;
|
|
156
|
-
tickDivisions: number[][];
|
|
157
|
-
minLabelDistance: number;
|
|
158
|
-
minLabelTickSize: number;
|
|
159
|
-
minTickDistance: number;
|
|
160
|
-
minTickStep: number;
|
|
161
|
-
tickSizes: number[];
|
|
162
|
-
normalized: boolean;
|
|
163
|
-
inverted: boolean;
|
|
164
|
-
minValue?: number;
|
|
165
|
-
maxValue?: number;
|
|
166
|
-
minValuePadded: number;
|
|
167
|
-
maxValuePadded: number;
|
|
168
|
-
stacks: Record<string, Stack>;
|
|
169
|
-
lowerDeadZone: number;
|
|
170
|
-
upperDeadZone: number;
|
|
171
|
-
origin: number;
|
|
172
|
-
scale: { factor: number; min: number; max: number; minPadding: number; maxPadding: number };
|
|
173
|
-
a: number;
|
|
174
|
-
b: number;
|
|
175
|
-
shouldUpdate: boolean;
|
|
176
|
-
|
|
177
|
-
reset(
|
|
178
|
-
min: number,
|
|
179
|
-
max: number,
|
|
180
|
-
snapToTicks: number,
|
|
181
|
-
tickDivisions: number[][],
|
|
182
|
-
minTickDistance: number,
|
|
183
|
-
minTickStep: number,
|
|
184
|
-
minLabelDistance: number,
|
|
185
|
-
minLabelTickSize: number,
|
|
186
|
-
normalized: boolean,
|
|
187
|
-
inverted: boolean,
|
|
188
|
-
lowerDeadZone: number,
|
|
189
|
-
upperDeadZone: number,
|
|
190
|
-
): void {
|
|
191
|
-
this.min = min;
|
|
192
|
-
this.max = max;
|
|
193
|
-
this.snapToTicks = snapToTicks;
|
|
194
|
-
this.tickDivisions = tickDivisions;
|
|
195
|
-
this.minLabelDistance = minLabelDistance;
|
|
196
|
-
this.minLabelTickSize = minLabelTickSize;
|
|
197
|
-
this.minTickDistance = minTickDistance;
|
|
198
|
-
this.minTickStep = minTickStep;
|
|
199
|
-
this.tickSizes = [];
|
|
200
|
-
this.normalized = normalized;
|
|
201
|
-
this.inverted = inverted;
|
|
202
|
-
delete this.minValue;
|
|
203
|
-
delete this.maxValue;
|
|
204
|
-
this.stacks = {};
|
|
205
|
-
this.lowerDeadZone = lowerDeadZone || 0;
|
|
206
|
-
this.upperDeadZone = upperDeadZone || 0;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
map(v: number, offset: number = 0): number {
|
|
210
|
-
return this.origin + (v + offset - this.scale.min + this.scale.minPadding) * this.scale.factor;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
decodeValue(n: number): number {
|
|
214
|
-
return n;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
encodeValue(v: number): number {
|
|
218
|
-
return v;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
constrainValue(v: number): number {
|
|
222
|
-
return Math.max(this.scale.min, Math.min(this.scale.max, v));
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
trackValue(v: number, offset: number = 0, constrain: boolean = false): number {
|
|
226
|
-
let value = (v - this.origin) / this.scale.factor - offset + this.scale.min - this.scale.minPadding;
|
|
227
|
-
if (constrain) value = this.constrainValue(v);
|
|
228
|
-
return value;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
hash(): any {
|
|
232
|
-
let r: any = {
|
|
233
|
-
origin: this.origin,
|
|
234
|
-
factor: this.scale.factor,
|
|
235
|
-
min: this.scale.min,
|
|
236
|
-
max: this.scale.max,
|
|
237
|
-
minPadding: this.scale.minPadding,
|
|
238
|
-
maxPadding: this.scale.maxPadding,
|
|
239
|
-
};
|
|
240
|
-
r.stacks = Object.keys(this.stacks)
|
|
241
|
-
.map((s) => this.stacks[s].info?.join(","))
|
|
242
|
-
.join(":");
|
|
243
|
-
return r;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
isSame(x: any): boolean {
|
|
247
|
-
let hash = this.hash();
|
|
248
|
-
let same = x && !Object.keys(hash).some((k) => x[k] !== hash[k]);
|
|
249
|
-
this.shouldUpdate = !same;
|
|
250
|
-
return same;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
measure(a: number, b: number): void {
|
|
254
|
-
this.a = a;
|
|
255
|
-
this.b = b;
|
|
256
|
-
|
|
257
|
-
if (this.minValue != null && this.min == null) this.min = this.minValue;
|
|
258
|
-
if (this.maxValue != null && this.max == null) this.max = this.maxValue;
|
|
259
|
-
|
|
260
|
-
for (let s in this.stacks) {
|
|
261
|
-
let info = this.stacks[s].measure(this.normalized);
|
|
262
|
-
let [min, max] = info;
|
|
263
|
-
if (this.min == null || min < this.min) this.min = min;
|
|
264
|
-
if (this.max == null || max > this.max) this.max = max;
|
|
265
|
-
this.stacks[s].info = info;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (this.min == null) this.min = 0;
|
|
269
|
-
if (this.max == null) this.max = this.normalized ? 1 : 100;
|
|
270
|
-
|
|
271
|
-
if (this.min == this.max) {
|
|
272
|
-
if (this.min == 0) {
|
|
273
|
-
this.min = -1;
|
|
274
|
-
this.max = 1;
|
|
275
|
-
} else {
|
|
276
|
-
let delta = Math.abs(this.min) * 0.1;
|
|
277
|
-
this.min -= delta;
|
|
278
|
-
this.max += delta;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
this.origin = this.inverted ? this.b : this.a;
|
|
283
|
-
|
|
284
|
-
this.scale = this.getScale();
|
|
285
|
-
|
|
286
|
-
this.calculateTicks();
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
getScale(tickSizes?: number[]): { factor: number; min: number; max: number; minPadding: number; maxPadding: number } {
|
|
290
|
-
let { min, max } = this;
|
|
291
|
-
let smin = min;
|
|
292
|
-
let smax = max;
|
|
293
|
-
|
|
294
|
-
let tickSize;
|
|
295
|
-
if (tickSizes && isNumber(this.snapToTicks) && tickSizes.length > 0) {
|
|
296
|
-
tickSize = tickSizes[Math.min(tickSizes.length - 1, this.snapToTicks)];
|
|
297
|
-
smin = Math.floor(smin / tickSize) * tickSize;
|
|
298
|
-
smax = Math.ceil(smax / tickSize) * tickSize;
|
|
299
|
-
} else {
|
|
300
|
-
if (this.minValue === min) smin = this.minValuePadded;
|
|
301
|
-
if (this.maxValue === max) smax = this.maxValuePadded;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
let minPadding = this.minValue === min ? Math.max(0, smin - this.minValuePadded) : 0;
|
|
305
|
-
let maxPadding = this.maxValue === max ? Math.max(0, this.maxValuePadded - smax) : 0;
|
|
306
|
-
|
|
307
|
-
let sign = this.b > this.a ? 1 : -1;
|
|
308
|
-
|
|
309
|
-
let factor =
|
|
310
|
-
smin < smax
|
|
311
|
-
? (Math.abs(this.b - this.a) - this.lowerDeadZone - this.upperDeadZone) /
|
|
312
|
-
(smax - smin + minPadding + maxPadding)
|
|
313
|
-
: 0;
|
|
314
|
-
|
|
315
|
-
if (factor < 0) factor = 0;
|
|
316
|
-
|
|
317
|
-
if (factor > 0 && (this.lowerDeadZone > 0 || this.upperDeadZone > 0)) {
|
|
318
|
-
while (factor * (min - smin) < this.lowerDeadZone) smin -= this.lowerDeadZone / factor;
|
|
319
|
-
|
|
320
|
-
while (factor * (smax - max) < this.upperDeadZone) smax += this.upperDeadZone / factor;
|
|
321
|
-
|
|
322
|
-
if (tickSize! > 0 && isNumber(this.snapToTicks)) {
|
|
323
|
-
smin = Math.floor(smin / tickSize!) * tickSize!;
|
|
324
|
-
smax = Math.ceil(smax / tickSize!) * tickSize!;
|
|
325
|
-
minPadding = this.minValue === min ? Math.max(0, smin - this.minValuePadded) : 0;
|
|
326
|
-
maxPadding = this.maxValue === max ? Math.max(0, this.maxValuePadded - smax) : 0;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
factor = smin < smax ? Math.abs(this.b - this.a) / (smax - smin + minPadding + maxPadding) : 0;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
return {
|
|
333
|
-
factor: sign * (this.inverted ? -factor : factor),
|
|
334
|
-
min: smin,
|
|
335
|
-
max: smax,
|
|
336
|
-
minPadding,
|
|
337
|
-
maxPadding,
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
acknowledge(value: number, width: number = 0, offset: number = 0): void {
|
|
342
|
-
if (value == null) return;
|
|
343
|
-
|
|
344
|
-
if (this.minValue == null || value < this.minValue) {
|
|
345
|
-
this.minValue = value;
|
|
346
|
-
this.minValuePadded = value + offset - width / 2;
|
|
347
|
-
}
|
|
348
|
-
if (this.maxValue == null || value > this.maxValue) {
|
|
349
|
-
this.maxValue = value;
|
|
350
|
-
this.maxValuePadded = value + offset + width / 2;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
getStack(name: string): Stack {
|
|
355
|
-
let s = this.stacks[name];
|
|
356
|
-
if (!s) s = this.stacks[name] = new Stack();
|
|
357
|
-
return s;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
stacknowledge(name: string, ordinal: any, value: any): any {
|
|
361
|
-
return this.getStack(name).acknowledge(ordinal, value);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
stack(name: string, ordinal: any, value: any): number | null {
|
|
365
|
-
let v = this.getStack(name).stack(ordinal, value);
|
|
366
|
-
return v != null ? this.map(v) : null;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
findTickSize(minPxDist: number): number | undefined {
|
|
370
|
-
return this.tickSizes.find((a) => a >= this.minLabelTickSize && a * Math.abs(this.scale.factor) >= minPxDist);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
getTickSizes(): number[] {
|
|
374
|
-
return this.tickSizes;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
calculateTicks(): void {
|
|
378
|
-
let dist = this.minLabelDistance / Math.abs(this.scale.factor);
|
|
379
|
-
let unit = Math.pow(10, Math.floor(Math.log10(dist)));
|
|
380
|
-
|
|
381
|
-
let bestLabelDistance = Infinity;
|
|
382
|
-
let bestTicks: number[] = [];
|
|
383
|
-
let bestScale = this.scale;
|
|
384
|
-
|
|
385
|
-
for (let i = 0; i < this.tickDivisions.length; i++) {
|
|
386
|
-
let divs = this.tickDivisions[i];
|
|
387
|
-
let tickSizes = divs.filter((ts) => ts >= this.minTickStep).map((ts) => ts * unit);
|
|
388
|
-
let scale = this.getScale(tickSizes);
|
|
389
|
-
tickSizes.forEach((size, level) => {
|
|
390
|
-
let labelDistance = size * Math.abs(scale.factor);
|
|
391
|
-
if (labelDistance >= this.minLabelDistance && labelDistance < bestLabelDistance) {
|
|
392
|
-
bestScale = scale;
|
|
393
|
-
bestTicks = tickSizes;
|
|
394
|
-
bestLabelDistance = labelDistance;
|
|
395
|
-
}
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
this.scale = bestScale;
|
|
399
|
-
this.tickSizes = bestTicks.filter(
|
|
400
|
-
(ts) => ts >= this.minTickStep && ts * Math.abs(bestScale.factor) >= this.minTickDistance,
|
|
401
|
-
);
|
|
402
|
-
if (this.tickSizes.length > 0) {
|
|
403
|
-
let max = this.tickSizes[this.tickSizes.length - 1];
|
|
404
|
-
this.tickSizes.push(2 * max);
|
|
405
|
-
this.tickSizes.push(5 * max);
|
|
406
|
-
this.tickSizes.push(10 * max);
|
|
407
|
-
let min = this.tickSizes[0];
|
|
408
|
-
let minDist = min * Math.abs(bestScale.factor);
|
|
409
|
-
if (min / 10 >= this.minTickStep && minDist / 10 >= this.minTickDistance) this.tickSizes.splice(0, 0, min / 10);
|
|
410
|
-
else if (min / 5 >= this.minTickStep && minDist / 5 >= this.minTickDistance) this.tickSizes.splice(0, 0, min / 5);
|
|
411
|
-
else if (min / 2 >= this.minTickStep && minDist / 2 >= this.minTickDistance) this.tickSizes.splice(0, 0, min / 2);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
getTicks(tickSizes: number[]): number[][] {
|
|
416
|
-
return tickSizes.map((size) => {
|
|
417
|
-
let start = Math.ceil((this.scale.min - this.scale.minPadding) / size);
|
|
418
|
-
let end = Math.floor((this.scale.max + this.scale.maxPadding) / size);
|
|
419
|
-
let result: number[] = [];
|
|
420
|
-
for (let i = start; i <= end; i++) result.push(i * size + 0);
|
|
421
|
-
return result;
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
mapGridlines(): number[] {
|
|
426
|
-
let size = this.tickSizes[0];
|
|
427
|
-
let start = Math.ceil((this.scale.min - this.scale.minPadding) / size);
|
|
428
|
-
let end = Math.floor((this.scale.max + this.scale.maxPadding) / size);
|
|
429
|
-
let result: number[] = [];
|
|
430
|
-
for (let i = start; i <= end; i++) result.push(this.map(i * size));
|
|
431
|
-
return result;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
book(): void {
|
|
435
|
-
Console.warn("NumericAxis does not support the autoSize flag for column and bar graphs.");
|
|
436
|
-
}
|
|
437
|
-
}
|