@genome-spy/core 0.61.0 → 0.62.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/schema.json CHANGED
@@ -2510,6 +2510,10 @@
2510
2510
  "FilterScoredLabelsParams": {
2511
2511
  "additionalProperties": false,
2512
2512
  "properties": {
2513
+ "asMidpoint": {
2514
+ "description": "Outputs the average of pos and pos2 as the midpoint of the element. This is useful for elements that have a width, such as transcripts. The midpoint is clamped to the visible region of the element.",
2515
+ "type": "string"
2516
+ },
2513
2517
  "channel": {
2514
2518
  "description": "**Default:** `\"x\"`",
2515
2519
  "enum": [
@@ -2520,7 +2524,7 @@
2520
2524
  },
2521
2525
  "lane": {
2522
2526
  "$ref": "#/definitions/Field",
2523
- "description": "An optional field representing element's lane, e.g., if transcripts are shown using a piled up layout."
2527
+ "description": "An optional field representing element's lane, e.g., if transcripts are shown using a piled up layout. Each line is processed separately."
2524
2528
  },
2525
2529
  "padding": {
2526
2530
  "description": "Padding (in pixels) around the element.\n\n**Default:** `0`",
@@ -2528,7 +2532,11 @@
2528
2532
  },
2529
2533
  "pos": {
2530
2534
  "$ref": "#/definitions/Field",
2531
- "description": "The field representing element's position on the domain."
2535
+ "description": "The field representing element's start position on the domain."
2536
+ },
2537
+ "pos2": {
2538
+ "$ref": "#/definitions/Field",
2539
+ "description": "The field representing element's end position on the domain. If not specified, the `pos` field is used."
2532
2540
  },
2533
2541
  "score": {
2534
2542
  "$ref": "#/definitions/Field",
@@ -2541,7 +2549,7 @@
2541
2549
  },
2542
2550
  "width": {
2543
2551
  "$ref": "#/definitions/Field",
2544
- "description": "The field representing element's width in pixels"
2552
+ "description": "The field representing element's width in pixels."
2545
2553
  }
2546
2554
  },
2547
2555
  "required": [
@@ -4554,7 +4562,14 @@
4554
4562
  "$ref": "#/definitions/Field"
4555
4563
  },
4556
4564
  "fontSize": {
4557
- "type": "number"
4565
+ "anyOf": [
4566
+ {
4567
+ "type": "number"
4568
+ },
4569
+ {
4570
+ "$ref": "#/definitions/ExprRef"
4571
+ }
4572
+ ]
4558
4573
  },
4559
4574
  "type": {
4560
4575
  "const": "measureText",
@@ -1 +1 @@
1
- {"version":3,"file":"collector.d.ts","sourceRoot":"","sources":["../../../src/data/collector.js"],"names":[],"mappings":"AAUA;;;;;GAKG;AACH;IAkCI;;OAEG;IACH,qBAFW,OAAO,sBAAsB,EAAE,aAAa,EAetD;IAVG,qDAA2C;IAE3C,2CAA2C;IAC3C,WADW,CAAC,CAAS,IAAS,EAAT,SAAS,KAAE,IAAI,CAAC,EAAE,CACpB;IAGnB,yFAAyF;IACzF,cADW,GAAG,CAAC,OAAO,oBAAoB,EAAE,MAAM,EAAE,+BAAO,CACN;IAwHzD;;OAEG;IACH,WAFa,QAAQ,+BAAO,CAqB3B;IAED;;;OAGG;IACH,mBAFW,CAAC,KAAK,+BAAO,KAAK,IAAI,QAUhC;IAED;;OAEG;IACH,uBAMC;IA8CD;;;;OAIG;IACH,8BAFW,MAAM,iCA4BhB;;CACJ;qBAhTyD,eAAe"}
1
+ {"version":3,"file":"collector.d.ts","sourceRoot":"","sources":["../../../src/data/collector.js"],"names":[],"mappings":"AAUA;;;;;GAKG;AACH;IAuCI;;OAEG;IACH,qBAFW,OAAO,sBAAsB,EAAE,aAAa,EAiBtD;IAZG,qDAA2C;IAE3C,2CAA2C;IAC3C,WADW,CAAC,CAAS,IAAS,EAAT,SAAS,KAAE,IAAI,CAAC,EAAE,CACpB;IAGnB,yFAAyF;IACzF,cADW,GAAG,CAAC,OAAO,oBAAoB,EAAE,MAAM,EAAE,+BAAO,CACN;IAgHzD;;OAEG;IACH,WAFa,QAAQ,+BAAO,CAqB3B;IAED;;;OAGG;IACH,mBAFW,CAAC,KAAK,+BAAO,KAAK,IAAI,QAUhC;IAED;;OAEG;IACH,uBAMC;IA8CD;;;;OAIG;IACH,8BAFW,MAAM,iCA4BhB;;CACJ;qBA7SyD,eAAe"}
@@ -40,6 +40,11 @@ export default class Collector extends FlowNode {
40
40
  */
41
41
  #facetIndices;
42
42
 
43
+ /**
44
+ * @type {(a: Datum, b: Datum) => number}
45
+ */
46
+ #comparator;
47
+
43
48
  get behavior() {
44
49
  return BEHAVIOR_COLLECTS;
45
50
  }
@@ -63,6 +68,8 @@ export default class Collector extends FlowNode {
63
68
  /** @type {Map<import("../spec/channel.js").Scalar[], Data>} TODO: proper type for key */
64
69
  this.facetBatches = new InternMap([], JSON.stringify);
65
70
 
71
+ this.#comparator = makeComparator(this.params?.sort);
72
+
66
73
  this.#init();
67
74
  }
68
75
 
@@ -100,18 +107,6 @@ export default class Collector extends FlowNode {
100
107
  // Free some memory
101
108
  this.#buffer = [];
102
109
 
103
- const sort = this.params?.sort;
104
- // Vega's "compare" function is incredibly slow (uses megamorphic field accessor)
105
- // TODO: Implement a replacement for static data types
106
- const comparator = sort ? compare(sort.field, sort.order) : undefined;
107
-
108
- /** @param {any[]} data */
109
- const sortData = (data) => {
110
- if (comparator) {
111
- data.sort(comparator);
112
- }
113
- };
114
-
115
110
  if (this.params.groupby?.length) {
116
111
  if (this.facetBatches.size > 1) {
117
112
  throw new Error("TODO: Support faceted data!");
@@ -137,9 +132,11 @@ export default class Collector extends FlowNode {
137
132
  }
138
133
  }
139
134
 
140
- for (const data of this.facetBatches.values()) {
141
- // TODO: Only sort if not already sorted
142
- sortData(data);
135
+ if (this.#comparator) {
136
+ for (const data of this.facetBatches.values()) {
137
+ // TODO: Only sort if not already sorted
138
+ data.sort(this.#comparator);
139
+ }
143
140
  }
144
141
 
145
142
  this.#buildUniqueIdIndex();
@@ -329,3 +326,31 @@ function groupBy(data, accessor) {
329
326
  }
330
327
  return groups;
331
328
  }
329
+
330
+ /**
331
+ * Creates a comparator function based on the provided sort parameters.
332
+ *
333
+ * @param {import("../spec/transform.js").CompareParams} sort
334
+ * @returns {(a: Datum, b: Datum) => number}
335
+ */
336
+ function makeComparator(sort) {
337
+ // For simple cases, create a simple comparator.
338
+ // For more complex cases, use Vega's compare function. However,
339
+ // is uses megamorphic field accessors, which makes it slow.
340
+ if (sort?.field) {
341
+ const fields = asArray(sort.field);
342
+ if (fields.length == 1 && !fields[0].includes(".")) {
343
+ const order = asArray(sort.order)[0] ?? "ascending";
344
+ const fieldName = JSON.stringify(fields[0]);
345
+ return /** @type {(a: Datum, b: Datum) => number} */ (
346
+ new Function(
347
+ "a",
348
+ "b",
349
+ `return a[${fieldName}] ${order === "ascending" ? "-" : "+"} b[${fieldName}];`
350
+ )
351
+ );
352
+ }
353
+
354
+ return compare(sort.field, sort.order);
355
+ }
356
+ }
@@ -9,8 +9,10 @@ export default class FilterScoredLabelsTransform extends Transform {
9
9
  /** @type {any[]} */
10
10
  _data: any[];
11
11
  channel: "x" | "y";
12
- posAccessor: import("vega-util").AccessorFn<any>;
13
- posBisector: import("d3-array").Bisector<any, any>;
12
+ startPosAccessor: import("vega-util").AccessorFn<any>;
13
+ endPosAccessor: import("vega-util").AccessorFn<any>;
14
+ startPosBisector: import("d3-array").Bisector<any, any>;
15
+ endPosBisector: import("d3-array").Bisector<any, any>;
14
16
  scoreAccessor: import("vega-util").AccessorFn<any>;
15
17
  widthAccessor: import("vega-util").AccessorFn<any>;
16
18
  /** @type {function(any):any} */
@@ -20,7 +22,6 @@ export default class FilterScoredLabelsTransform extends Transform {
20
22
  reservationMaps: Map<any, ReservationMap>;
21
23
  resolution: import("../../view/scaleResolution.js").default;
22
24
  schedule: () => void;
23
- _scores: any[];
24
25
  _filterAndPropagate(): void;
25
26
  groups: Map<any, any>;
26
27
  }
@@ -1 +1 @@
1
- {"version":3,"file":"filterScoredLabels.d.ts","sourceRoot":"","sources":["../../../../src/data/transforms/filterScoredLabels.js"],"names":[],"mappings":"AAOA;IAKI;;;;OAIG;IACH,oBAHW,OAAO,yBAAyB,EAAE,wBAAwB,QAC1D,OAAO,oBAAoB,EAAE,OAAO,EA2C9C;IAtCG,mEAAoB;IAEpB,oBAAoB;IACpB,OADW,GAAG,EAAE,CACD;IAEf,mBAAoC;IAMpC,iDAAyC;IACzC,mDAA6C;IAC7C,mDAA6C;IAC7C,mDAA6C;IAC7C,gCAAgC;IAChC,cADW,CAAS,IAAG,EAAH,GAAG,KAAE,GAAG,CAGd;IACd,gBAAuC;IAEvC,uCAAuC;IACvC,iBADW,GAAG,CAAC,GAAG,EAAE,cAAc,CAAC,CACH;IAEhC,4DAAuD;IAIvD,qBAAuE;IAgBvE,eAAiD;IAWrD,4BAyCC;IAKG,sBAAuB;CAU9B;sBA9HqB,gBAAgB;2BAFX,+BAA+B"}
1
+ {"version":3,"file":"filterScoredLabels.d.ts","sourceRoot":"","sources":["../../../../src/data/transforms/filterScoredLabels.js"],"names":[],"mappings":"AAOA;IAKI;;;;OAIG;IACH,oBAHW,OAAO,yBAAyB,EAAE,wBAAwB,QAC1D,OAAO,oBAAoB,EAAE,OAAO,EA6C9C;IAxCG,mEAAoB;IAEpB,oBAAoB;IACpB,OADW,GAAG,EAAE,CACD;IAEf,mBAAoC;IAMpC,sDAA8C;IAC9C,oDAAgE;IAChE,wDAAuD;IACvD,sDAAmD;IACnD,mDAA6C;IAC7C,mDAA6C;IAC7C,gCAAgC;IAChC,cADW,CAAS,IAAG,EAAH,GAAG,KAAE,GAAG,CAGd;IACd,gBAAuC;IAEvC,uCAAuC;IACvC,iBADW,GAAG,CAAC,GAAG,EAAE,cAAc,CAAC,CACH;IAEhC,4DAAuD;IAIvD,qBAAuE;IAyB3E,4BAsEC;IAKG,sBAAuB;CAU9B;sBA3JqB,gBAAgB;2BAFX,+BAA+B"}
@@ -1,6 +1,6 @@
1
1
  import { bisector } from "d3-array";
2
2
  import { BEHAVIOR_COLLECTS } from "../flowNode.js";
3
- import { topKSlice } from "../../utils/topK.js";
3
+ import { topK } from "../../utils/topK.js";
4
4
  import ReservationMap from "../../utils/reservationMap.js";
5
5
  import { field } from "../../utils/field.js";
6
6
  import Transform from "./transform.js";
@@ -29,8 +29,10 @@ export default class FilterScoredLabelsTransform extends Transform {
29
29
  throw new Error("Invalid channel: " + this.channel);
30
30
  }
31
31
 
32
- this.posAccessor = field(this.params.pos);
33
- this.posBisector = bisector(this.posAccessor);
32
+ this.startPosAccessor = field(this.params.pos);
33
+ this.endPosAccessor = field(this.params.pos2 ?? this.params.pos);
34
+ this.startPosBisector = bisector(this.startPosAccessor);
35
+ this.endPosBisector = bisector(this.endPosAccessor);
34
36
  this.scoreAccessor = field(this.params.score);
35
37
  this.widthAccessor = field(this.params.width);
36
38
  /** @type {function(any):any} */
@@ -59,10 +61,8 @@ export default class FilterScoredLabelsTransform extends Transform {
59
61
  }
60
62
 
61
63
  complete() {
62
- const posAccessor = this.posAccessor;
63
- this._data.sort((a, b) => posAccessor(a) - posAccessor(b));
64
-
65
- this._scores = this._data.map(this.scoreAccessor);
64
+ const startPosAccessor = this.startPosAccessor;
65
+ this._data.sort((a, b) => startPosAccessor(a) - startPosAccessor(b));
66
66
 
67
67
  for (const lane of new Set(this._data.map(this.laneAccessor))) {
68
68
  this.reservationMaps.set(lane, new ReservationMap(200));
@@ -91,25 +91,54 @@ export default class FilterScoredLabelsTransform extends Transform {
91
91
  const k = 70; // TODO: Configurable
92
92
 
93
93
  // Find the maximum of k elements from the visible domain in priority order
94
- const topIndices = topKSlice(
95
- this._scores,
94
+ const topElements = topK(
95
+ this._data,
96
96
  k,
97
- this.posBisector.left(this._data, domain[0]),
98
- this.posBisector.right(this._data, domain[1])
97
+ this.scoreAccessor,
98
+ this.endPosBisector.left(this._data, domain[0]),
99
+ this.startPosBisector.right(this._data, domain[1])
99
100
  );
100
101
 
101
102
  // Try to fit the elements on the available lanes and propagate if there was room
102
- for (const i of topIndices) {
103
- const datum = this._data[i];
104
- const pos = scale(this.posAccessor(datum)) * rangeSpan;
105
- const halfWidth = this.widthAccessor(datum) / 2 + this.padding;
103
+ for (const datum of topElements) {
104
+ let startPos = scale(this.startPosAccessor(datum)) * rangeSpan;
105
+ let endPos = scale(this.endPosAccessor(datum)) * rangeSpan;
106
+
107
+ const span = endPos - startPos;
108
+ const width = this.widthAccessor(datum) + this.padding * 2;
109
+
110
+ let midpoint = (startPos + endPos) / 2;
111
+
112
+ // How much extra space we have for adjusting the position so that the
113
+ // text stays inside the range.
114
+ const extra = Math.max(0.0, (span - width) / 2.0);
115
+ if (extra > 0.0) {
116
+ const leftOver = Math.max(0.0, width / 2 - midpoint);
117
+ midpoint += Math.min(leftOver, extra);
118
+
119
+ const rightOver = Math.max(
120
+ 0.0,
121
+ width / 2 + midpoint - rangeSpan
122
+ );
123
+ midpoint -= Math.min(rightOver, extra);
124
+ }
106
125
 
107
126
  if (
108
127
  this.reservationMaps
109
128
  .get(this.laneAccessor(datum))
110
- .reserve(pos - halfWidth, pos + halfWidth)
129
+ .reserve(midpoint - width / 2, midpoint + width / 2)
111
130
  ) {
112
- this._propagate(datum);
131
+ if (this.params.asMidpoint) {
132
+ // Clone the datum to avoid side effects
133
+ const clonedDatum = Object.assign({}, datum);
134
+ // @ts-ignore
135
+ clonedDatum[this.params.asMidpoint] = scale.invert(
136
+ midpoint / rangeSpan
137
+ );
138
+ this._propagate(clonedDatum);
139
+ } else {
140
+ this._propagate(datum);
141
+ }
113
142
  }
114
143
  }
115
144
 
@@ -1,13 +1,13 @@
1
1
  /**
2
- * Measures text length. This is mainly intended for reading-direction arrows
3
- * in gene annotations.
2
+ * Measures text length. This is mainly intended for strand arrows in gene annotations.
4
3
  */
5
4
  export default class MeasureTextTransform extends Transform {
6
5
  /**
7
6
  *
8
7
  * @param {import("../../spec/transform.js").MeasureTextParams} params
8
+ * @param {import("../flowNode.js").ParamMediatorProvider} paramMediatorProvider
9
9
  */
10
- constructor(params: import("../../spec/transform.js").MeasureTextParams);
10
+ constructor(params: import("../../spec/transform.js").MeasureTextParams, paramMediatorProvider: import("../flowNode.js").ParamMediatorProvider);
11
11
  params: import("../../spec/transform.js").MeasureTextParams;
12
12
  /**
13
13
  *
@@ -1 +1 @@
1
- {"version":3,"file":"measureText.d.ts","sourceRoot":"","sources":["../../../../src/data/transforms/measureText.js"],"names":[],"mappings":"AAMA;;;GAGG;AACH;IAKI;;;OAGG;IACH,oBAFW,OAAO,yBAAyB,EAAE,iBAAiB,EA0B7D;IArBG,4DAAoB;IAQpB;;;OAGG;IACH,gBAFW,GAAG,UAUb;CAER;sBAxCqB,gBAAgB"}
1
+ {"version":3,"file":"measureText.d.ts","sourceRoot":"","sources":["../../../../src/data/transforms/measureText.js"],"names":[],"mappings":"AAOA;;GAEG;AACH;IAKI;;;;OAIG;IACH,oBAHW,OAAO,yBAAyB,EAAE,iBAAiB,yBACnD,OAAO,gBAAgB,EAAE,qBAAqB,EA0CxD;IArCG,4DAAoB;IAwBpB;;;OAGG;IACH,gBAFW,GAAG,UAUb;CAER;sBAzDqB,gBAAgB"}
@@ -3,10 +3,10 @@ import fontMetadata from "../../fonts/Lato-Regular.json" with { type: "json" };
3
3
  import getMetrics from "../../fonts/bmFontMetrics.js";
4
4
  import { field } from "../../utils/field.js";
5
5
  import Transform from "./transform.js";
6
+ import { isExprRef } from "../../view/paramMediator.js";
6
7
 
7
8
  /**
8
- * Measures text length. This is mainly intended for reading-direction arrows
9
- * in gene annotations.
9
+ * Measures text length. This is mainly intended for strand arrows in gene annotations.
10
10
  */
11
11
  export default class MeasureTextTransform extends Transform {
12
12
  get behavior() {
@@ -16,8 +16,9 @@ export default class MeasureTextTransform extends Transform {
16
16
  /**
17
17
  *
18
18
  * @param {import("../../spec/transform.js").MeasureTextParams} params
19
+ * @param {import("../flowNode.js").ParamMediatorProvider} paramMediatorProvider
19
20
  */
20
- constructor(params) {
21
+ constructor(params, paramMediatorProvider) {
21
22
  super(params);
22
23
 
23
24
  this.params = params;
@@ -26,7 +27,23 @@ export default class MeasureTextTransform extends Transform {
26
27
  const accessor = field(params.field);
27
28
  const as = params.as;
28
29
  // TODO: Support custom fonts.
29
- const size = params.fontSize;
30
+
31
+ let size = 0;
32
+
33
+ // TODO: Refactor this into reusable code.
34
+ if (isExprRef(params.fontSize)) {
35
+ const sizeExpr =
36
+ paramMediatorProvider.paramMediator.createExpression(
37
+ params.fontSize.expr
38
+ );
39
+ size = sizeExpr();
40
+ sizeExpr.addListener(() => {
41
+ size = sizeExpr();
42
+ this.repropagate();
43
+ });
44
+ } else {
45
+ size = params.fontSize;
46
+ }
30
47
 
31
48
  /**
32
49
  *
@@ -1 +1 @@
1
- {"version":3,"file":"point.d.ts","sourceRoot":"","sources":["../../../src/marks/point.js"],"names":[],"mappings":"AAmBA;;GAEG;AACH;IAGI;;OAEG;IACH,sBAFW,OAAO,qBAAqB,EAAE,OAAO,EA6C/C;IA0EO,iDAMC;IAkET,+BAkBC;;CAgDJ;iBAhRgB,WAAW"}
1
+ {"version":3,"file":"point.d.ts","sourceRoot":"","sources":["../../../src/marks/point.js"],"names":[],"mappings":"AAmBA;;GAEG;AACH;IAGI;;OAEG;IACH,sBAFW,OAAO,qBAAqB,EAAE,OAAO,EAmD/C;IA0EO,iDAMC;IAkET,+BAkBC;;CAgDJ;iBAtRgB,WAAW"}
@@ -69,6 +69,12 @@ export default class PointMark extends Mark {
69
69
  this.#semanticZoomFraction = () => szf;
70
70
  }
71
71
  }
72
+
73
+ if ("geometricZoomBound" in this.properties) {
74
+ console.warn(
75
+ 'geometricZoomBound is deprecated. Use something like the following instead: "size": { "expr": "min(0.5 * pow(zoomLevel, 2), 200)" }.'
76
+ );
77
+ }
72
78
  }
73
79
 
74
80
  /**
@@ -1,4 +1,5 @@
1
1
  import { PositionalChannel } from "./channel.js";
2
+ import { ExprRef } from "./parameter.js";
2
3
 
3
4
  /**
4
5
  * The name of the field or a JavaScript expression for accessing nested properties.
@@ -425,7 +426,7 @@ export interface MeasureTextParams extends TransformParamsBase {
425
426
 
426
427
  field: Field;
427
428
 
428
- fontSize: number;
429
+ fontSize: number | ExprRef;
429
430
 
430
431
  as: string;
431
432
 
@@ -484,18 +485,31 @@ export interface FilterScoredLabelsParams extends TransformParamsBase {
484
485
  score: Field;
485
486
 
486
487
  /**
487
- * The field representing element's width in pixels
488
+ * The field representing element's width in pixels.
488
489
  */
489
490
  width: Field;
490
491
 
491
492
  /**
492
- * The field representing element's position on the domain.
493
+ * The field representing element's start position on the domain.
493
494
  */
494
495
  pos: Field;
495
496
 
497
+ /**
498
+ * The field representing element's end position on the domain.
499
+ * If not specified, the `pos` field is used.
500
+ */
501
+ pos2?: Field;
502
+
503
+ /**
504
+ * Outputs the average of pos and pos2 as the midpoint of the element.
505
+ * This is useful for elements that have a width, such as transcripts.
506
+ * The midpoint is clamped to the visible region of the element.
507
+ */
508
+ asMidpoint?: string;
509
+
496
510
  /**
497
511
  * An optional field representing element's lane, e.g., if transcripts
498
- * are shown using a piled up layout.
512
+ * are shown using a piled up layout. Each line is processed separately.
499
513
  */
500
514
  lane?: Field;
501
515
 
@@ -1,22 +1,13 @@
1
1
  /**
2
- * Finds the top k
3
- *
4
- * Based on ideas at https://lemire.me/blog/2017/06/21/top-speed-for-top-k-queries/
2
+ * Finds the top k elements in a slice of the data array, using a priority accessor.
5
3
  *
6
4
  * @param {T[]} data
7
5
  * @param {number} k
8
6
  * @param {(datum: T) => number} priorityAccessor
9
- * @template T
10
- */
11
- export function topK<T>(data: T[], k: number, priorityAccessor: (datum: T) => number): T[];
12
- /**
13
- * Takes an array of priorities and returns the top k indices from the
14
- * specified slice
15
- *
16
- * @param {number[]} priorities An array of priorities
17
- * @param {number} k
18
7
  * @param {number} [start] Default: 0
19
- * @param {number} [end] Exclusive. Default: priorities.length
8
+ * @param {number} [end] Exclusive. Default: data.length
9
+ * @template T
10
+ * @returns {T[]}
20
11
  */
21
- export function topKSlice(priorities: number[], k: number, start?: number, end?: number): number[];
12
+ export function topK<T>(data: T[], k: number, priorityAccessor?: (datum: T) => number, start?: number, end?: number): T[];
22
13
  //# sourceMappingURL=topK.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"topK.d.ts","sourceRoot":"","sources":["../../../src/utils/topK.js"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,qBAFa,CAAC,QAHH,CAAC,EAAE,KACH,MAAM,oBACN,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,OA4B9B;AAED;;;;;;;;GAQG;AACH,sCALW,MAAM,EAAE,KACR,MAAM,UACN,MAAM,QACN,MAAM,YA6BhB"}
1
+ {"version":3,"file":"topK.d.ts","sourceRoot":"","sources":["../../../src/utils/topK.js"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AACH,qBAHa,CAAC,QALH,CAAC,EAAE,KACH,MAAM,qBACN,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,UACpB,MAAM,QACN,MAAM,GAEJ,CAAC,EAAE,CAiCf"}
@@ -1,64 +1,34 @@
1
1
  import FlatQueue from "flatqueue";
2
2
 
3
3
  /**
4
- * Finds the top k
5
- *
6
- * Based on ideas at https://lemire.me/blog/2017/06/21/top-speed-for-top-k-queries/
4
+ * Finds the top k elements in a slice of the data array, using a priority accessor.
7
5
  *
8
6
  * @param {T[]} data
9
7
  * @param {number} k
10
8
  * @param {(datum: T) => number} priorityAccessor
11
- * @template T
12
- */
13
- export function topK(data, k, priorityAccessor) {
14
- /** @type {FlatQueue<number>} */
15
- const queue = new FlatQueue();
16
-
17
- let i;
18
- for (i = 0; i < k && i < data.length; i++) {
19
- queue.push(i, priorityAccessor(data[i]));
20
- }
21
-
22
- for (; i < data.length; i++) {
23
- const p = priorityAccessor(data[i]);
24
- if (p >= queue.peekValue()) {
25
- queue.push(i, p);
26
- queue.pop();
27
- }
28
- }
29
-
30
- const result = [];
31
-
32
- let index;
33
- while ((index = queue.pop()) !== undefined) {
34
- result.push(data[index]);
35
- }
36
-
37
- return result.reverse();
38
- }
39
-
40
- /**
41
- * Takes an array of priorities and returns the top k indices from the
42
- * specified slice
43
- *
44
- * @param {number[]} priorities An array of priorities
45
- * @param {number} k
46
9
  * @param {number} [start] Default: 0
47
- * @param {number} [end] Exclusive. Default: priorities.length
10
+ * @param {number} [end] Exclusive. Default: data.length
11
+ * @template T
12
+ * @returns {T[]}
48
13
  */
49
- export function topKSlice(priorities, k, start = 0, end = priorities.length) {
14
+ export function topK(
15
+ data,
16
+ k,
17
+ priorityAccessor = (x) => +x,
18
+ start = 0,
19
+ end = data.length
20
+ ) {
50
21
  /** @type {FlatQueue<number>} */
51
22
  const queue = new FlatQueue();
52
-
53
23
  const sliceLength = end - start;
54
24
 
55
25
  let i;
56
26
  for (i = 0; i < k && i < sliceLength; i++) {
57
- queue.push(i, priorities[start + i]);
27
+ queue.push(i, priorityAccessor(data[start + i]));
58
28
  }
59
29
 
60
30
  for (; i < sliceLength; i++) {
61
- const p = priorities[start + i];
31
+ const p = priorityAccessor(data[start + i]);
62
32
  if (p >= queue.peekValue()) {
63
33
  queue.push(i, p);
64
34
  queue.pop();
@@ -66,10 +36,9 @@ export function topKSlice(priorities, k, start = 0, end = priorities.length) {
66
36
  }
67
37
 
68
38
  const result = [];
69
-
70
39
  let index;
71
40
  while ((index = queue.pop()) !== undefined) {
72
- result.push(start + index);
41
+ result.push(data[start + index]);
73
42
  }
74
43
 
75
44
  return result.reverse();
@@ -1,6 +1,5 @@
1
1
  import { expect, test } from "vitest";
2
- import { range } from "d3-array";
3
- import { topK, topKSlice } from "./topK.js";
2
+ import { topK } from "./topK.js";
4
3
 
5
4
  test("topK returns top k numbers in priority order", () => {
6
5
  /** @param {number} x */
@@ -16,49 +15,40 @@ test("topK returns top k numbers in priority order", () => {
16
15
  expect(topK([1, 1, 1], 3, priorityAccessor)).toEqual([1, 1, 1]);
17
16
  });
18
17
 
19
- test("topK returns top k objects in priority order", () => {
20
- /** @param {{priority: number}} d */
21
- const priorityAccessor = (d) => d.priority;
18
+ test("topK returns top k objects in priority order within a start-end range", () => {
19
+ const arr = [0, 9, 1, 8, 2, 7, 3, 6, 4, 5].map((x) => ({ priority: x }));
20
+ const priorityAccessor = (/** @type {{priority: number}} */ d) =>
21
+ d.priority;
22
22
 
23
- expect(
24
- topK(
25
- [0, 9, 1, 8, 2, 7, 3, 6, 4, 5].map((x) => ({ priority: x })),
26
- 3,
27
- priorityAccessor
28
- )
29
- ).toEqual([9, 8, 7].map((x) => ({ priority: x })));
30
- });
31
-
32
- test("topK returns top k objects in priority order with large datasets", () => {
33
- /** @param {number} x */
34
- const priorityAccessor = (x) => x;
23
+ // Range: indices 2 to 8 (1,8,2,7,3,6)
24
+ expect(topK(arr, 2, priorityAccessor, 2, 8)).toEqual([
25
+ { priority: 8 },
26
+ { priority: 7 },
27
+ ]);
35
28
 
36
- const n = 10000;
37
- const bigArray = range(n).map((x) => Math.floor(Math.random() * 100));
38
- const sortedBigArray = bigArray.slice().sort((a, b) => b - a);
29
+ // Range: indices 4 to 10 (2,7,3,6,4,5)
30
+ expect(topK(arr, 3, priorityAccessor, 4, 10)).toEqual([
31
+ { priority: 7 },
32
+ { priority: 6 },
33
+ { priority: 5 },
34
+ ]);
39
35
 
40
- for (let k = 0; k < 13000; k += 1000) {
41
- expect(topK(bigArray, k, priorityAccessor)).toEqual(
42
- sortedBigArray.slice(0, k)
43
- );
44
- }
36
+ // Range: indices 0 to 3 (0,9,1)
37
+ expect(topK(arr, 2, priorityAccessor, 0, 3)).toEqual([
38
+ { priority: 9 },
39
+ { priority: 1 },
40
+ ]);
45
41
  });
46
42
 
47
- test("topKSlice returns top k indexes in priority order", () => {
48
- expect(topKSlice([0, 1, 2], 3)).toEqual([2, 1, 0]);
49
- expect(topKSlice([1, 2, 3], 3)).toEqual([2, 1, 0]);
50
- expect(topKSlice([0, 1, 2], 1)).toEqual([2]);
51
- expect(topKSlice([0, 1, 2], 6)).toEqual([2, 1, 0]);
52
- expect(topKSlice([0, 1, 2, 3, 4, 5], 3)).toEqual([5, 4, 3]);
53
- expect(topKSlice([0, 9, 1, 8, 2, 7, 3, 6, 4, 5], 3)).toEqual([1, 3, 5]);
54
- expect(new Set(topKSlice([1, 1, 1, 2, 2, 2], 3))).toEqual(
55
- new Set([3, 4, 5])
56
- );
43
+ test("topK returns empty array if start >= end", () => {
44
+ const arr = [1, 2, 3, 4, 5];
45
+ expect(topK(arr, 3, (x) => x, 4, 4)).toEqual([]);
46
+ expect(topK(arr, 3, (x) => x, 5, 5)).toEqual([]);
47
+ expect(topK(arr, 3, (x) => x, 6, 6)).toEqual([]);
57
48
  });
58
49
 
59
- test("topKSlice returns top k indexes from a slice in priority order", () => {
60
- expect(topKSlice([0, 1, 2, 3, 4, 5], 2, 1, 5)).toEqual([4, 3]);
61
- expect(topKSlice([0, 9, 1, 8, 2, 7, 3, 6, 4, 5], 3, 1, 5)).toEqual([
62
- 1, 3, 4,
63
- ]);
50
+ test("topK works with negative and zero priorities in a range", () => {
51
+ const arr = [-10, 0, 5, -2, 3, 0, -1];
52
+ expect(topK(arr, 2, (x) => x, 1, 6)).toEqual([5, 3]);
53
+ expect(topK(arr, 3, (x) => x, 0, 4)).toEqual([5, 0, -2]);
64
54
  });
@@ -1 +1 @@
1
- {"version":3,"file":"scaleResolution.d.ts","sourceRoot":"","sources":["../../../src/view/scaleResolution.js"],"names":[],"mappings":"AA4+BA;;;;;;;;;;GAUG;AACH,6CAFW,OAAO,WAAW,EAAE,OAAO,GAAG,OAAO,WAAW,EAAE,OAAO,EAAE,QA4BrE;AAp+BD,2BAA4B,cAAc,CAAC;AAC3C,sBAAuB,SAAS,CAAC;AACjC,sBAAuB,SAAS,CAAC;AACjC,oBAAqB,OAAO,CAAC;AAC7B,oBAAqB,OAAO,CAAC;AAE7B;;;;;;;;GAQG;AACH;;;;;;;GAOG;AACH;IA4CI;;OAEG;IACH,2DASC;IARG,8CAAsB;IACtB,yDAAyD;IACzD,SADW,qBAAqB,EAAE,CACjB;IACjB,0FAA0F;IAC1F,MADW,OAAO,oBAAoB,EAAE,IAAI,CAC5B;IAEhB,iEAAiE;IACjE,MADW,MAAM,CACI;IAWzB,2BAMC;IAED;;;;;;;OAOG;IACH,4KAEC;IAED;;;OAGG;IACH,+KAEC;IAcD;;;;;OAKG;IACH,qBAFW,qBAAqB,QAqD/B;IA8MD;;;;OAIG;IACH,+DAOC;IAED;;OAEG;IACH,oBAsCC;IAED;;OAEG;IACH;eA7ZkC,OAAO,kBAAkB,EAAE,KAAK;MAqcjE;IAED,mBAEC;IAED;;OAEG;IACH,oBAFa,mFAA6B,CAOzC;IAED;;;;OAIG;IACH,oBAKC;IAED;;OAEG;IACH,sBAGC;IAUD;;;;;;;OAOG;IACH,kBALW,MAAM,eACN,MAAM,OACN,MAAM,GACJ,OAAO,CAmEnB;IAED;;;;;;OAMG;IACH,eAJW,mFAA6B,aAC7B,OAAO,GAAG,MAAM,iBA0D1B;IAED;;;;OAIG;IACH,qBAcC;IAED;;;;;OAKG;IACH,uBAOC;IAED;;;;;;;OAOG;IACH,wBAoBC;IAiED;;;OAGG;IACH,aAFa,OAAO,qBAAqB,EAAE,OAAO,CAajD;IAID;;;;;OAKG;IACH,uBAFW,MAAM,yDAUhB;IAED;;OAEG;IACH,iBAFW,MAAM,yDAKhB;IAED;;;OAGG;IACH,qBAHW,MAAM,+CAAmB,GACvB,MAAM,CAQlB;IAED;;;OAGG;IACH,8BAHW,kFAA4B,GAC1B,MAAM,EAAE,CAOpB;;CACJ;kCA91B+B,CAAC,SAApB,6CAAkB;;;;UAGrB,OAAO,eAAe,EAAE,OAAO;aAC/B,CAAC;gBACD,OAAO,oBAAoB,EAAE,mBAAmB;sBAChD,CAAC,OAAO,+CAAkB,EAAE,IAAI,EAAE,OAAO,oBAAoB,EAAE,IAAI,kDAAgB"}
1
+ {"version":3,"file":"scaleResolution.d.ts","sourceRoot":"","sources":["../../../src/view/scaleResolution.js"],"names":[],"mappings":"AAm/BA;;;;;;;;;;GAUG;AACH,6CAFW,OAAO,WAAW,EAAE,OAAO,GAAG,OAAO,WAAW,EAAE,OAAO,EAAE,QA4BrE;AA3+BD,2BAA4B,cAAc,CAAC;AAC3C,sBAAuB,SAAS,CAAC;AACjC,sBAAuB,SAAS,CAAC;AACjC,oBAAqB,OAAO,CAAC;AAC7B,oBAAqB,OAAO,CAAC;AAE7B;;;;;;;;GAQG;AACH;;;;;;;GAOG;AACH;IA4CI;;OAEG;IACH,2DASC;IARG,8CAAsB;IACtB,yDAAyD;IACzD,SADW,qBAAqB,EAAE,CACjB;IACjB,0FAA0F;IAC1F,MADW,OAAO,oBAAoB,EAAE,IAAI,CAC5B;IAEhB,iEAAiE;IACjE,MADW,MAAM,CACI;IAWzB,2BAMC;IAED;;;;;;;OAOG;IACH,4KAEC;IAED;;;OAGG;IACH,+KAEC;IAcD;;;;;OAKG;IACH,qBAFW,qBAAqB,QAqD/B;IA8MD;;;;OAIG;IACH,+DAOC;IAED;;OAEG;IACH,oBA6CC;IAED;;OAEG;IACH;eApakC,OAAO,kBAAkB,EAAE,KAAK;MA4cjE;IAED,mBAEC;IAED;;OAEG;IACH,oBAFa,mFAA6B,CAOzC;IAED;;;;OAIG;IACH,oBAKC;IAED;;OAEG;IACH,sBAGC;IAUD;;;;;;;OAOG;IACH,kBALW,MAAM,eACN,MAAM,OACN,MAAM,GACJ,OAAO,CAmEnB;IAED;;;;;;OAMG;IACH,eAJW,mFAA6B,aAC7B,OAAO,GAAG,MAAM,iBA0D1B;IAED;;;;OAIG;IACH,qBAcC;IAED;;;;;OAKG;IACH,uBAOC;IAED;;;;;;;OAOG;IACH,wBAoBC;IAiED;;;OAGG;IACH,aAFa,OAAO,qBAAqB,EAAE,OAAO,CAajD;IAID;;;;;OAKG;IACH,uBAFW,MAAM,yDAUhB;IAED;;OAEG;IACH,iBAFW,MAAM,yDAKhB;IAED;;;OAGG;IACH,qBAHW,MAAM,+CAAmB,GACvB,MAAM,CAQlB;IAED;;;OAGG;IACH,8BAHW,kFAA4B,GAC1B,MAAM,EAAE,CAOpB;;CACJ;kCAr2B+B,CAAC,SAApB,6CAAkB;;;;UAGrB,OAAO,eAAe,EAAE,OAAO;aAC/B,CAAC;gBACD,OAAO,oBAAoB,EAAE,mBAAmB;sBAChD,CAAC,OAAO,+CAAkB,EAAE,IAAI,EAAE,OAAO,oBAAoB,EAAE,IAAI,kDAAgB"}
@@ -469,6 +469,13 @@ export default class ScaleResolution {
469
469
  scale.props = props;
470
470
  this.#configureRange();
471
471
 
472
+ if (!this.#initialDomain && isContinuous(scale.type)) {
473
+ const domain = scale.domain();
474
+ if (span(domain) > 0) {
475
+ this.#initialDomain = domain;
476
+ }
477
+ }
478
+
472
479
  if (!domainWasInitialized) {
473
480
  this.#initialDomain = scale.domain();
474
481
  this.#notifyListeners("domain");