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/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
- data.sorters[0].value = isDefined(sortColumn.sortValue) ? sortColumn.sortValue : sortColumn.value;
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) ? hdwidget.sortValue : hdwidget.value;
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
- let value = isDefined(column.sortValue) ? column.sortValue : column.value;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cx",
3
- "version": "26.7.6",
3
+ "version": "26.8.0",
4
4
  "description": "Advanced JavaScript UI framework for admin and dashboard applications with ready to use grid, form and chart components.",
5
5
  "exports": {
6
6
  "./data": {
@@ -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
- /** Set to `true` to enable smooth (curved) line rendering. */
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
- /** Controls the curvature of smooth lines. Value should be between 0 and 0.4. Default is 0.05. */
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.forEach((p, i) => {
308
- linePath +=
309
- i == 0
310
- ? `M ${p.x} ${p.y}`
311
- : !data.smooth || span.length < 2
312
- ? `L ${p.x} ${p.y}`
313
- : this.getCurvedPathSegment(p, span, i - 1, i - 2, i - 1, i + 1, r);
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
- let closePath = "";
330
- span.forEach((p, i) => {
331
- let segment = "";
332
- if (i == 0) {
333
- segment = `M ${p.x} ${p.y}`;
334
-
335
- // closing point
336
- closePath =
337
- !data.smooth || span.length < 2
338
- ? `L ${p.x} ${p.y0}`
339
- : this.getCurvedPathSegment(p, span, i + 1, i + 2, i + 1, i - 1, r, "y0");
340
- } else {
341
- if (!data.smooth) {
342
- segment = `L ${p.x} ${p.y}`;
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
- getCurvedPathSegment(
378
- p: LinePoint,
379
- points: LinePoint[],
380
- i1: number,
381
- i2: number,
382
- j1: number,
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
- getControlPoint({
394
- cp,
395
- pp,
396
- np,
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
- getLineInfo(p1x: number, p1y: number, p2x: number, p2y: number): { length: number; angle: number } {
424
- const lengthX = p2x - p1x;
425
- const lengthY = p2y - p1y;
370
+ if (n == 2) {
371
+ m[0] = m[1] = secant(0);
372
+ return m;
373
+ }
426
374
 
427
- return {
428
- length: Math.sqrt(Math.pow(lengthX, 2) + Math.pow(lengthY, 2)),
429
- angle: Math.atan2(lengthY, lengthX),
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 { Stack } from "./Stack";
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
- }