@genome-spy/core 0.74.0 → 0.76.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.
Files changed (143) hide show
  1. package/dist/bundle/{esm-CgfVIRJ-.js → esm-BimDEpBb.js} +1 -1
  2. package/dist/bundle/{esm-DtE8VqAv.js → esm-Bvlm1uVk.js} +1 -1
  3. package/dist/bundle/{esm-sIoQYZ21.js → esm-CngqBe45.js} +17 -17
  4. package/dist/bundle/{esm-DQiq2Zhd.js → esm-D_euN86T.js} +43 -43
  5. package/dist/bundle/index.es.js +6064 -5756
  6. package/dist/bundle/index.js +104 -103
  7. package/dist/schema.json +572 -12
  8. package/dist/src/config/defaults/markDefaults.d.ts.map +1 -1
  9. package/dist/src/config/defaults/markDefaults.js +1 -12
  10. package/dist/src/config/defaults/scaleDefaults.d.ts.map +1 -1
  11. package/dist/src/config/defaults/scaleDefaults.js +1 -0
  12. package/dist/src/config/markConfig.d.ts.map +1 -1
  13. package/dist/src/config/markConfig.js +16 -8
  14. package/dist/src/config/themes.d.ts.map +1 -1
  15. package/dist/src/config/themes.js +15 -2
  16. package/dist/src/data/sources/dataUtils.d.ts +25 -0
  17. package/dist/src/data/sources/dataUtils.d.ts.map +1 -1
  18. package/dist/src/data/sources/dataUtils.js +23 -0
  19. package/dist/src/data/sources/inlineSource.js +2 -2
  20. package/dist/src/data/sources/lazy/registerBuiltInLazySources.js +2 -2
  21. package/dist/src/data/sources/lazy/registerCoreLazySources.d.ts +2 -0
  22. package/dist/src/data/sources/lazy/registerCoreLazySources.d.ts.map +1 -0
  23. package/dist/src/data/sources/lazy/registerCoreLazySources.js +2 -0
  24. package/dist/src/data/sources/lazy/tabixSource.d.ts +7 -0
  25. package/dist/src/data/sources/lazy/tabixSource.d.ts.map +1 -1
  26. package/dist/src/data/sources/lazy/tabixSource.js +18 -0
  27. package/dist/src/data/sources/lazy/tabixTsvSource.d.ts +37 -0
  28. package/dist/src/data/sources/lazy/tabixTsvSource.d.ts.map +1 -0
  29. package/dist/src/data/sources/lazy/tabixTsvSource.js +163 -0
  30. package/dist/src/data/sources/urlSource.d.ts.map +1 -1
  31. package/dist/src/data/sources/urlSource.js +8 -3
  32. package/dist/src/encoder/encoder.d.ts +2 -2
  33. package/dist/src/encoder/encoder.d.ts.map +1 -1
  34. package/dist/src/genome/scaleLocus.d.ts.map +1 -1
  35. package/dist/src/genome/scaleLocus.js +8 -3
  36. package/dist/src/genomeSpy/interactionController.d.ts.map +1 -1
  37. package/dist/src/genomeSpy/interactionController.js +91 -51
  38. package/dist/src/genomeSpyBase.d.ts.map +1 -1
  39. package/dist/src/genomeSpyBase.js +4 -1
  40. package/dist/src/gl/dataToVertices.d.ts +12 -14
  41. package/dist/src/gl/dataToVertices.d.ts.map +1 -1
  42. package/dist/src/gl/dataToVertices.js +116 -95
  43. package/dist/src/gl/glslScaleGenerator.d.ts +3 -0
  44. package/dist/src/gl/glslScaleGenerator.d.ts.map +1 -1
  45. package/dist/src/gl/glslScaleGenerator.js +10 -8
  46. package/dist/src/gl/vertexRangeIndex.d.ts +23 -0
  47. package/dist/src/gl/vertexRangeIndex.d.ts.map +1 -0
  48. package/dist/src/gl/vertexRangeIndex.js +150 -0
  49. package/dist/src/gl/webGLHelper.d.ts +5 -2
  50. package/dist/src/gl/webGLHelper.d.ts.map +1 -1
  51. package/dist/src/gl/webGLHelper.js +20 -3
  52. package/dist/src/marks/__snapshots__/shaderSnapshot.test.js.snap +1082 -0
  53. package/dist/src/marks/link.vertex.glsl.js +1 -1
  54. package/dist/src/marks/mark.d.ts +1 -1
  55. package/dist/src/minimal.d.ts.map +1 -1
  56. package/dist/src/minimal.js +5 -4
  57. package/dist/src/paramRuntime/expressionCompiler.d.ts +2 -1
  58. package/dist/src/paramRuntime/expressionCompiler.d.ts.map +1 -1
  59. package/dist/src/paramRuntime/expressionCompiler.js +3 -2
  60. package/dist/src/paramRuntime/expressionRef.d.ts +4 -1
  61. package/dist/src/paramRuntime/expressionRef.d.ts.map +1 -1
  62. package/dist/src/paramRuntime/expressionRef.js +10 -3
  63. package/dist/src/paramRuntime/graphRuntime.d.ts.map +1 -1
  64. package/dist/src/paramRuntime/graphRuntime.js +15 -6
  65. package/dist/src/paramRuntime/paramRuntime.d.ts +8 -2
  66. package/dist/src/paramRuntime/paramRuntime.d.ts.map +1 -1
  67. package/dist/src/paramRuntime/paramRuntime.js +10 -5
  68. package/dist/src/paramRuntime/types.d.ts +1 -0
  69. package/dist/src/paramRuntime/types.d.ts.map +1 -1
  70. package/dist/src/paramRuntime/types.js +1 -0
  71. package/dist/src/paramRuntime/viewParamRuntime.d.ts +5 -4
  72. package/dist/src/paramRuntime/viewParamRuntime.d.ts.map +1 -1
  73. package/dist/src/paramRuntime/viewParamRuntime.js +17 -6
  74. package/dist/src/scale/scale.d.ts.map +1 -1
  75. package/dist/src/scale/scale.js +11 -2
  76. package/dist/src/scales/domainPlanner.d.ts +57 -11
  77. package/dist/src/scales/domainPlanner.d.ts.map +1 -1
  78. package/dist/src/scales/domainPlanner.js +183 -84
  79. package/dist/src/scales/scaleInstanceManager.d.ts.map +1 -1
  80. package/dist/src/scales/scaleInstanceManager.js +7 -2
  81. package/dist/src/scales/scalePropsResolver.d.ts +3 -3
  82. package/dist/src/scales/scalePropsResolver.d.ts.map +1 -1
  83. package/dist/src/scales/scalePropsResolver.js +28 -5
  84. package/dist/src/scales/scaleResolution.d.ts +12 -1
  85. package/dist/src/scales/scaleResolution.d.ts.map +1 -1
  86. package/dist/src/scales/scaleResolution.js +180 -21
  87. package/dist/src/scales/selectionDomainUtils.d.ts +10 -0
  88. package/dist/src/scales/selectionDomainUtils.d.ts.map +1 -1
  89. package/dist/src/scales/selectionDomainUtils.js +32 -3
  90. package/dist/src/screenshotExport.d.ts +23 -0
  91. package/dist/src/screenshotExport.d.ts.map +1 -0
  92. package/dist/src/screenshotExport.js +44 -0
  93. package/dist/src/screenshotHarness.d.ts.map +1 -1
  94. package/dist/src/screenshotHarness.js +26 -24
  95. package/dist/src/spec/axis.d.ts +2 -2
  96. package/dist/src/spec/channel.d.ts +34 -4
  97. package/dist/src/spec/data.d.ts +52 -0
  98. package/dist/src/spec/parameter.d.ts +6 -0
  99. package/dist/src/spec/scale.d.ts +13 -1
  100. package/dist/src/spec/transform.d.ts +6 -0
  101. package/dist/src/utils/expression.d.ts +16 -8
  102. package/dist/src/utils/expression.d.ts.map +1 -1
  103. package/dist/src/utils/expression.js +291 -11
  104. package/dist/src/view/axisGridView.d.ts.map +1 -1
  105. package/dist/src/view/axisGridView.js +2 -1
  106. package/dist/src/view/axisView.d.ts.map +1 -1
  107. package/dist/src/view/axisView.js +2 -1
  108. package/dist/src/view/facetView.d.ts.map +1 -1
  109. package/dist/src/view/facetView.js +2 -1
  110. package/dist/src/view/flowBuilder.d.ts +1 -1
  111. package/dist/src/view/flowBuilder.d.ts.map +1 -1
  112. package/dist/src/view/flowBuilder.js +11 -7
  113. package/dist/src/view/gridView/gridChild.d.ts.map +1 -1
  114. package/dist/src/view/gridView/gridChild.js +9 -1
  115. package/dist/src/view/gridView/gridView.d.ts.map +1 -1
  116. package/dist/src/view/gridView/gridView.js +198 -32
  117. package/dist/src/view/gridView/scrollbar.d.ts.map +1 -1
  118. package/dist/src/view/gridView/scrollbar.js +5 -1
  119. package/dist/src/view/gridView/selectionRect.d.ts.map +1 -1
  120. package/dist/src/view/gridView/selectionRect.js +5 -1
  121. package/dist/src/view/gridView/separatorView.d.ts.map +1 -1
  122. package/dist/src/view/gridView/separatorView.js +5 -1
  123. package/dist/src/view/resolutionPlanner.d.ts +9 -0
  124. package/dist/src/view/resolutionPlanner.d.ts.map +1 -0
  125. package/dist/src/view/resolutionPlanner.js +302 -0
  126. package/dist/src/view/testUtils.d.ts +30 -3
  127. package/dist/src/view/testUtils.d.ts.map +1 -1
  128. package/dist/src/view/testUtils.js +51 -2
  129. package/dist/src/view/unitView.d.ts +1 -1
  130. package/dist/src/view/unitView.d.ts.map +1 -1
  131. package/dist/src/view/unitView.js +5 -152
  132. package/dist/src/view/view.d.ts.map +1 -1
  133. package/dist/src/view/view.js +2 -1
  134. package/dist/src/view/viewSelectors.d.ts +38 -10
  135. package/dist/src/view/viewSelectors.d.ts.map +1 -1
  136. package/dist/src/view/viewSelectors.js +67 -2
  137. package/dist/src/view/viewUtilTypes.d.ts +15 -0
  138. package/dist/src/view/viewUtils.d.ts.map +1 -1
  139. package/dist/src/view/viewUtils.js +10 -0
  140. package/package.json +2 -2
  141. package/LICENSE +0 -21
  142. /package/dist/bundle/{esm-BDFRLEuD.js → esm-C49STiCR.js} +0 -0
  143. /package/dist/bundle/{esm-CGX-qz1d.js → esm-CuVa5T98.js} +0 -0
@@ -28,15 +28,17 @@ import {
28
28
  isPrimaryPositionalChannel,
29
29
  isSecondaryChannel,
30
30
  } from "../encoder/encoder.js";
31
+ import { isExprRef } from "../paramRuntime/paramUtils.js";
31
32
  import { NominalDomain } from "../utils/domainArray.js";
32
33
  import { shallowArrayEquals } from "../utils/arrayUtils.js";
33
34
  import createIndexer from "../utils/indexer.js";
34
35
  import { getCachedOrCall, invalidate } from "../utils/propertyCacher.js";
35
36
  import { resolveUrl } from "../utils/url.js";
37
+ import { orderResolutionMembers } from "./resolutionMemberOrder.js";
36
38
  import {
37
39
  findIntervalSelectionBindingOwners,
40
+ getIntervalSelection,
38
41
  normalizeIntervalForSelection,
39
- requireIntervalSelection,
40
42
  } from "./selectionDomainUtils.js";
41
43
  import { toExternalIndexLikeInterval } from "./indexLikeDomainUtils.js";
42
44
 
@@ -100,6 +102,9 @@ export default class ScaleResolution {
100
102
  /** @type {Set<ScaleResolutionMember>} */
101
103
  #dataDomainMembers = new Set();
102
104
 
105
+ /** @type {ScaleResolutionMember[] | undefined} */
106
+ #orderedMembers;
107
+
103
108
  /**
104
109
  * @type {Record<ScaleResolutionEventType, Set<ScaleResolutionListener>>}
105
110
  */
@@ -127,15 +132,28 @@ export default class ScaleResolution {
127
132
 
128
133
  #selectionReverseSyncSuppressionDepth = 0;
129
134
 
135
+ /** @type {(() => void)[]} */
136
+ #configuredDomainExprUnsubscribers = [];
137
+
130
138
  #ignoreSelectionInitial = false;
131
139
 
132
140
  /** @type {[number, number] | null | undefined} */
133
141
  #lastLinkedSelectionInterval = undefined;
134
142
 
143
+ /** @type {import("../view/view.js").default | undefined} */
144
+ #hostView;
145
+
146
+ #resolvingScaleProps = 0;
147
+
148
+ #memberRegistrationBatchDepth = 0;
149
+
150
+ #membersDirty = false;
151
+
135
152
  /**
136
153
  * @param {Channel} channel
154
+ * @param {import("../view/view.js").default} [hostView]
137
155
  */
138
- constructor(channel) {
156
+ constructor(channel, hostView) {
139
157
  this.channel = channel;
140
158
  /** @type {import("../spec/channel.js").Type} Data type (quantitative, nominal, etc...) */
141
159
  this.type = null;
@@ -143,8 +161,10 @@ export default class ScaleResolution {
143
161
  /** @type {string} An optional unique identifier for the scale */
144
162
  this.name = undefined;
145
163
 
164
+ this.#hostView = hostView;
165
+
146
166
  this.#domainAggregator = new DomainPlanner({
147
- getMembers: () => this.#getActiveMembers(),
167
+ getActiveMembers: () => this.#getActiveMembers(),
148
168
  getAllMembers: () => this.#members,
149
169
  getDataMembers: () =>
150
170
  this.#getActiveMembers(this.#dataDomainMembers),
@@ -154,7 +174,7 @@ export default class ScaleResolution {
154
174
  });
155
175
 
156
176
  this.#scaleManager = new ScaleInstanceManager({
157
- getParamRuntime: () => this.#firstMemberView.paramRuntime,
177
+ getParamRuntime: () => this.#resolutionView.paramRuntime,
158
178
  onRangeChange: () => this.#notifyListeners("range"),
159
179
  onDomainChange: () => this.#notifyListeners("domain"),
160
180
  getGenomeStore: () => this.#viewContext.genomeStore,
@@ -182,6 +202,10 @@ export default class ScaleResolution {
182
202
  return first.view;
183
203
  }
184
204
 
205
+ get #resolutionView() {
206
+ return this.#hostView ?? this.#firstMemberView;
207
+ }
208
+
185
209
  /**
186
210
  * @param {Set<ScaleResolutionMember>} [members]
187
211
  */
@@ -206,7 +230,7 @@ export default class ScaleResolution {
206
230
  }
207
231
 
208
232
  get #viewContext() {
209
- return this.#firstMemberView.context;
233
+ return this.#resolutionView.context;
210
234
  }
211
235
 
212
236
  get zoomExtent() {
@@ -300,10 +324,13 @@ export default class ScaleResolution {
300
324
  return;
301
325
  }
302
326
 
303
- const selection = requireIntervalSelection(
327
+ const selection = getIntervalSelection(
304
328
  linkInfo.runtime.getValue(linkInfo.param),
305
329
  linkInfo.param
306
330
  );
331
+ if (!selection) {
332
+ return;
333
+ }
307
334
 
308
335
  const interval = this.#normalizeDomainIntervalForLinkedSelection(
309
336
  this.getScale().domain()
@@ -372,9 +399,15 @@ export default class ScaleResolution {
372
399
  * @returns {boolean}
373
400
  */
374
401
  #hasConfiguredDomain() {
375
- return this.#domainAggregator.hasConfiguredDomain({
376
- includeSelectionInitial: this.#shouldIncludeSelectionInitial(),
377
- });
402
+ for (const member of this.#members) {
403
+ if (
404
+ member.contributesToDomain &&
405
+ member.channelDef.scale?.domain !== undefined
406
+ ) {
407
+ return true;
408
+ }
409
+ }
410
+ return false;
378
411
  }
379
412
 
380
413
  /**
@@ -382,10 +415,13 @@ export default class ScaleResolution {
382
415
  * @returns {[number, number] | null}
383
416
  */
384
417
  #getCurrentLinkedSelectionInterval(linkInfo) {
385
- const selection = requireIntervalSelection(
418
+ const selection = getIntervalSelection(
386
419
  linkInfo.runtime.getValue(linkInfo.param),
387
420
  linkInfo.param
388
421
  );
422
+ if (!selection) {
423
+ return null;
424
+ }
389
425
  const interval = selection.intervals[linkInfo.encoding];
390
426
  return interval && interval.length === 2
391
427
  ? /** @type {[number, number]} */ (interval)
@@ -484,9 +520,53 @@ export default class ScaleResolution {
484
520
  if (member.contributesToDomain) {
485
521
  this.#dataDomainMembers.add(member);
486
522
  }
523
+ return member;
524
+ }
525
+
526
+ #syncMembers() {
527
+ this.#membersDirty = false;
528
+ this.#invalidateOrderedMembers();
487
529
  this.#invalidateConfiguredDomain();
488
530
  this.#refreshSelectionDomainParamSubscriptions();
489
- return member;
531
+ this.#refreshConfiguredDomainExprSubscriptions();
532
+ }
533
+
534
+ #markMembersDirty() {
535
+ if (this.#memberRegistrationBatchDepth > 0) {
536
+ this.#membersDirty = true;
537
+ } else {
538
+ this.#syncMembers();
539
+ }
540
+ }
541
+
542
+ /**
543
+ * Executes a group of member registrations without refreshing derived
544
+ * membership state until the callback completes.
545
+ *
546
+ * @template T
547
+ * @param {Iterable<ScaleResolution>} resolutions
548
+ * @param {() => T} callback
549
+ * @returns {T}
550
+ */
551
+ static registerInBatch(resolutions, callback) {
552
+ const batchedResolutions = Array.from(resolutions);
553
+ for (const resolution of batchedResolutions) {
554
+ resolution.#memberRegistrationBatchDepth++;
555
+ }
556
+
557
+ try {
558
+ return callback();
559
+ } finally {
560
+ for (const resolution of batchedResolutions) {
561
+ resolution.#memberRegistrationBatchDepth--;
562
+ if (
563
+ resolution.#memberRegistrationBatchDepth === 0 &&
564
+ resolution.#membersDirty
565
+ ) {
566
+ resolution.#syncMembers();
567
+ }
568
+ }
569
+ }
490
570
  }
491
571
 
492
572
  /**
@@ -495,12 +575,12 @@ export default class ScaleResolution {
495
575
  */
496
576
  registerMember(member) {
497
577
  const registeredMember = this.#addMember(member);
578
+ this.#markMembersDirty();
498
579
  return () => {
499
580
  const removed = this.#members.delete(registeredMember);
500
581
  if (removed) {
501
582
  this.#dataDomainMembers.delete(registeredMember);
502
- this.#invalidateConfiguredDomain();
503
- this.#refreshSelectionDomainParamSubscriptions();
583
+ this.#markMembersDirty();
504
584
  }
505
585
  return removed && this.#members.size === 0;
506
586
  };
@@ -508,6 +588,7 @@ export default class ScaleResolution {
508
588
 
509
589
  dispose() {
510
590
  this.#clearSelectionDomainParamSubscriptions();
591
+ this.#clearConfiguredDomainExprSubscriptions();
511
592
  this.#listeners.domain.clear();
512
593
  this.#listeners.range.clear();
513
594
  this.#scaleManager.dispose();
@@ -521,6 +602,13 @@ export default class ScaleResolution {
521
602
  this.#lastLinkedSelectionInterval = undefined;
522
603
  }
523
604
 
605
+ #clearConfiguredDomainExprSubscriptions() {
606
+ for (const unsubscribe of this.#configuredDomainExprUnsubscribers) {
607
+ unsubscribe();
608
+ }
609
+ this.#configuredDomainExprUnsubscribers = [];
610
+ }
611
+
524
612
  #refreshSelectionDomainParamSubscriptions() {
525
613
  this.#clearSelectionDomainParamSubscriptions();
526
614
 
@@ -552,6 +640,33 @@ export default class ScaleResolution {
552
640
  );
553
641
  }
554
642
 
643
+ #refreshConfiguredDomainExprSubscriptions() {
644
+ this.#clearConfiguredDomainExprSubscriptions();
645
+
646
+ if (this.#members.size === 0) {
647
+ return;
648
+ }
649
+
650
+ const listener = () => {
651
+ this.#invalidateConfiguredDomain();
652
+ this.reconfigureDomain();
653
+ };
654
+
655
+ for (const member of this.#members) {
656
+ if (!member.contributesToDomain) {
657
+ continue;
658
+ }
659
+ const domain = member.channelDef.scale?.domain;
660
+ if (!isExprRef(domain)) {
661
+ continue;
662
+ }
663
+
664
+ const expr = member.view.paramRuntime.createExpression(domain.expr);
665
+ const unsubscribe = expr.subscribe(listener);
666
+ this.#configuredDomainExprUnsubscribers.push(unsubscribe);
667
+ }
668
+ }
669
+
555
670
  #hasRenderedMember() {
556
671
  for (const member of this.#members) {
557
672
  if (member.view.hasRendered()) {
@@ -637,9 +752,9 @@ export default class ScaleResolution {
637
752
  const props = resolveScalePropsBase({
638
753
  channel: this.channel,
639
754
  dataType: this.type,
640
- members: this.#members,
755
+ orderedMembers: this.#getOrderedMembers(),
641
756
  isExplicitDomain: this.isDomainDefinedExplicitly(),
642
- configScopes: this.#firstMemberView.getConfigScopes(),
757
+ configScopes: this.#resolutionView.getConfigScopes(),
643
758
  });
644
759
  this.#validateLinkedSelectionConfiguration(props);
645
760
  return props;
@@ -650,6 +765,26 @@ export default class ScaleResolution {
650
765
  invalidate(this, "mergedScaleProps");
651
766
  }
652
767
 
768
+ #invalidateOrderedMembers() {
769
+ this.#orderedMembers = undefined;
770
+ }
771
+
772
+ /**
773
+ * Returns the participating members in a stable order.
774
+ *
775
+ * The membership set changes rarely, so cache the sorted order separately
776
+ * from merged scale props. That keeps parameter-driven domain updates from
777
+ * re-running the same path-based sort work.
778
+ *
779
+ * @returns {ScaleResolutionMember[]}
780
+ */
781
+ #getOrderedMembers() {
782
+ if (!this.#orderedMembers) {
783
+ this.#orderedMembers = orderResolutionMembers(this.#members);
784
+ }
785
+ return this.#orderedMembers;
786
+ }
787
+
653
788
  #invalidateConfiguredDomain() {
654
789
  this.#domainAggregator.invalidateConfiguredDomain();
655
790
  this.#invalidateMergedScaleProps();
@@ -736,10 +871,18 @@ export default class ScaleResolution {
736
871
 
737
872
  const resolvedProps = { ...props };
738
873
 
739
- const domain = this.#getConfiguredOrDefaultDomain(
740
- extractDataDomain,
741
- resolvedProps.type === LOCUS ? resolvedProps.assembly : undefined
742
- );
874
+ this.#resolvingScaleProps += 1;
875
+ let domain;
876
+ try {
877
+ domain = this.#getConfiguredOrDefaultDomain(
878
+ extractDataDomain,
879
+ resolvedProps.type === LOCUS
880
+ ? resolvedProps.assembly
881
+ : undefined
882
+ );
883
+ } finally {
884
+ this.#resolvingScaleProps -= 1;
885
+ }
743
886
 
744
887
  if (isDiscrete(resolvedProps.type)) {
745
888
  const isExplicit = this.isDomainDefinedExplicitly();
@@ -833,7 +976,6 @@ export default class ScaleResolution {
833
976
  */
834
977
  reconfigureDomain() {
835
978
  this.#withSelectionReverseSyncSuppressed(() => {
836
- this.#invalidateMergedScaleProps();
837
979
  const state = this.#computeScaleState(true, true);
838
980
  if (!state) {
839
981
  return;
@@ -946,6 +1088,11 @@ export default class ScaleResolution {
946
1088
  return;
947
1089
  }
948
1090
 
1091
+ if (scale.props.domainTransition === false) {
1092
+ this.#notifyListeners("domain");
1093
+ return;
1094
+ }
1095
+
949
1096
  const newDomain = scale.domain();
950
1097
  const action = this.#interactionController.getDomainChangeAction(
951
1098
  previousDomain,
@@ -1005,6 +1152,11 @@ export default class ScaleResolution {
1005
1152
  * @returns {ScaleWithProps}
1006
1153
  */
1007
1154
  getScale() {
1155
+ if (this.#resolvingScaleProps > 0) {
1156
+ throw new Error(
1157
+ `Scale resolution for channel "${this.channel}" cannot read its own scale while its domain is being resolved.`
1158
+ );
1159
+ }
1008
1160
  return this.#scaleManager.scale ?? this.initializeScale();
1009
1161
  }
1010
1162
 
@@ -1025,6 +1177,13 @@ export default class ScaleResolution {
1025
1177
  }
1026
1178
 
1027
1179
  getDomain() {
1180
+ if (this.#resolvingScaleProps > 0) {
1181
+ throw new Error(
1182
+ `Scale resolution for channel "${this.channel}" cannot read its own domain while its domain is being resolved.`
1183
+ );
1184
+ }
1185
+ // The underlying scale getter returns a fresh array. Treat this as a
1186
+ // read-only snapshot rather than a mutable backing store.
1028
1187
  return this.getScale().domain();
1029
1188
  }
1030
1189
 
@@ -1058,7 +1217,7 @@ export default class ScaleResolution {
1058
1217
  return;
1059
1218
  }
1060
1219
 
1061
- const root = this.#firstMemberView.getLayoutAncestors().at(-1);
1220
+ const root = this.#resolutionView.getLayoutAncestors().at(-1);
1062
1221
  const persist = root
1063
1222
  ? findIntervalSelectionBindingOwners(
1064
1223
  root,
@@ -10,6 +10,16 @@ export function requireParamRuntime(paramRuntime: {
10
10
  * @param {string} paramName
11
11
  */
12
12
  export function requireIntervalSelection(selection: any, paramName: string): import("../types/selectionTypes.js").IntervalSelection;
13
+ /**
14
+ * Returns an interval selection when the value exists and is of the correct
15
+ * type. Missing values are treated as empty so selection-linked domains can
16
+ * initialize before a pushed outer selection has been seeded.
17
+ *
18
+ * @param {any} selection
19
+ * @param {string} paramName
20
+ * @returns {import("../types/selectionTypes.js").IntervalSelection | undefined}
21
+ */
22
+ export function getIntervalSelection(selection: any, paramName: string): import("../types/selectionTypes.js").IntervalSelection | undefined;
13
23
  /**
14
24
  * Resolves the runtime-backed interval selection binding used by a linked
15
25
  * domain. Matching is based on the actual resolved runtime slot instead of
@@ -1 +1 @@
1
- {"version":3,"file":"selectionDomainUtils.d.ts","sourceRoot":"","sources":["../../../src/scales/selectionDomainUtils.js"],"names":[],"mappings":"AAOA;;;GAGG;AACH,kDAHW;IAAE,mBAAmB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,GAAG,CAAA;CAAE,aAC9C,MAAM,OAUhB;AAED;;;GAGG;AACH,oDAHW,GAAG,aACH,MAAM,0DAgBhB;AAED;;;;;;;;GAQG;AACH,sDAJW,OAAO,iBAAiB,EAAE,OAAO,aACjC,MAAM,YACN,GAAG,GAAG,GAAG;;;EAiBnB;AAED;;;;;GAKG;AACH,yDALW,OAAO,iBAAiB,EAAE,OAAO,WACjC,GAAG,aACH,MAAM,YACN,GAAG,GAAG,GAAG;UAGG,OAAO,iBAAiB,EAAE,OAAO;WAAS,OAAO,sBAAsB,EAAE,kBAAkB;IAuBjH;AAED;;;;;GAKG;AACH,yDALW,OAAO,iBAAiB,EAAE,OAAO,WACjC,GAAG,aACH,MAAM,YACN,GAAG,GAAG,GAAG,WA8CnB;AAED;;;;;GAKG;AACH,wDALW,MAAM,EAAE,cACR,MAAM,EAAE,YACR;IAAE,eAAe,CAAC,EAAE,OAAO,CAAA;CAAE,GAC3B,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAsCxC"}
1
+ {"version":3,"file":"selectionDomainUtils.d.ts","sourceRoot":"","sources":["../../../src/scales/selectionDomainUtils.js"],"names":[],"mappings":"AAOA;;;GAGG;AACH,kDAHW;IAAE,mBAAmB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,GAAG,CAAA;CAAE,aAC9C,MAAM,OAUhB;AAED;;;GAGG;AACH,oDAHW,GAAG,aACH,MAAM,0DAgBhB;AAED;;;;;;;;GAQG;AACH,gDAJW,GAAG,aACH,MAAM,GACJ,OAAO,4BAA4B,EAAE,iBAAiB,GAAG,SAAS,CAc9E;AAED;;;;;;;;GAQG;AACH,sDAJW,OAAO,iBAAiB,EAAE,OAAO,aACjC,MAAM,YACN,GAAG,GAAG,GAAG;;;EAiBnB;AAED;;;;;GAKG;AACH,yDALW,OAAO,iBAAiB,EAAE,OAAO,WACjC,GAAG,aACH,MAAM,YACN,GAAG,GAAG,GAAG;UAQG,OAAO,iBAAiB,EAAE,OAAO;WAAS,OAAO,sBAAsB,EAAE,kBAAkB;IAuBjH;AAED;;;;;GAKG;AACH,yDALW,OAAO,iBAAiB,EAAE,OAAO,WACjC,GAAG,aACH,MAAM,YACN,GAAG,GAAG,GAAG,WA+CnB;AAED;;;;;GAKG;AACH,wDALW,MAAM,EAAE,cACR,MAAM,EAAE,YACR;IAAE,eAAe,CAAC,EAAE,OAAO,CAAA;CAAE,GAC3B,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAsCxC"}
@@ -39,6 +39,29 @@ export function requireIntervalSelection(selection, paramName) {
39
39
  return selection;
40
40
  }
41
41
 
42
+ /**
43
+ * Returns an interval selection when the value exists and is of the correct
44
+ * type. Missing values are treated as empty so selection-linked domains can
45
+ * initialize before a pushed outer selection has been seeded.
46
+ *
47
+ * @param {any} selection
48
+ * @param {string} paramName
49
+ * @returns {import("../types/selectionTypes.js").IntervalSelection | undefined}
50
+ */
51
+ export function getIntervalSelection(selection, paramName) {
52
+ if (!selection) {
53
+ return;
54
+ }
55
+
56
+ if (!isIntervalSelection(selection)) {
57
+ throw new Error(
58
+ `Selection domain parameter "${paramName}" must be an interval selection.`
59
+ );
60
+ }
61
+
62
+ return selection;
63
+ }
64
+
42
65
  /**
43
66
  * Resolves the runtime-backed interval selection binding used by a linked
44
67
  * domain. Matching is based on the actual resolved runtime slot instead of
@@ -52,7 +75,7 @@ export function resolveIntervalSelectionBinding(view, paramName, encoding) {
52
75
  const runtime = view.paramRuntime.findRuntimeForParam
53
76
  ? requireParamRuntime(view.paramRuntime, paramName)
54
77
  : view.paramRuntime;
55
- const selection = requireIntervalSelection(
78
+ const selection = getIntervalSelection(
56
79
  runtime.getValue
57
80
  ? runtime.getValue(paramName)
58
81
  : view.paramRuntime.findValue(paramName),
@@ -71,7 +94,12 @@ export function resolveIntervalSelectionBinding(view, paramName, encoding) {
71
94
  * @param {string} paramName
72
95
  * @param {"x" | "y"} encoding
73
96
  */
74
- export function findIntervalSelectionBindingOwners(root, runtime, paramName, encoding) {
97
+ export function findIntervalSelectionBindingOwners(
98
+ root,
99
+ runtime,
100
+ paramName,
101
+ encoding
102
+ ) {
75
103
  /** @type {{ view: import("../view/view.js").default, param: import("../spec/parameter.js").SelectionParameter }[]} */
76
104
  const owners = [];
77
105
 
@@ -124,7 +152,8 @@ export function hasIntervalSelectionBindingInScope(
124
152
 
125
153
  seen.add(candidateView);
126
154
 
127
- const param = candidateView.paramRuntime?.paramConfigs?.get(paramName);
155
+ const param =
156
+ candidateView.paramRuntime?.paramConfigs?.get(paramName);
128
157
  if (!param || !isSelectionParameter(param)) {
129
158
  continue;
130
159
  }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @param {{ width: number | undefined, height: number | undefined }} renderedBounds
3
+ * @param {{ width: number, height: number }} logicalSize
4
+ */
5
+ export function resolveExportSize(renderedBounds: {
6
+ width: number | undefined;
7
+ height: number | undefined;
8
+ }, logicalSize: {
9
+ width: number;
10
+ height: number;
11
+ }): {
12
+ width: number;
13
+ height: number;
14
+ };
15
+ /**
16
+ * Returns the smallest DPR that yields at least 400 physical pixels vertically.
17
+ *
18
+ * Fractional DPRs are rounded upward to the nearest 0.5.
19
+ *
20
+ * @param {number} logicalHeight
21
+ */
22
+ export function resolveCaptureDevicePixelRatio(logicalHeight: number): number;
23
+ //# sourceMappingURL=screenshotExport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screenshotExport.d.ts","sourceRoot":"","sources":["../../src/screenshotExport.js"],"names":[],"mappings":"AAEA;;;GAGG;AACH,kDAHW;IAAE,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,eACzD;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE;;;EAiB3C;AAED;;;;;;GAMG;AACH,8DAFW,MAAM,UAehB"}
@@ -0,0 +1,44 @@
1
+ const MIN_VERTICAL_RESOLUTION = 400;
2
+
3
+ /**
4
+ * @param {{ width: number | undefined, height: number | undefined }} renderedBounds
5
+ * @param {{ width: number, height: number }} logicalSize
6
+ */
7
+ export function resolveExportSize(renderedBounds, logicalSize) {
8
+ return {
9
+ width:
10
+ Number.isFinite(renderedBounds.width) && renderedBounds.width > 0
11
+ ? Math.ceil(renderedBounds.width)
12
+ : Number.isFinite(logicalSize.width) && logicalSize.width > 0
13
+ ? logicalSize.width
14
+ : 500,
15
+ height:
16
+ Number.isFinite(renderedBounds.height) && renderedBounds.height > 0
17
+ ? Math.ceil(renderedBounds.height)
18
+ : Number.isFinite(logicalSize.height) && logicalSize.height > 0
19
+ ? logicalSize.height
20
+ : 280,
21
+ };
22
+ }
23
+
24
+ /**
25
+ * Returns the smallest DPR that yields at least 400 physical pixels vertically.
26
+ *
27
+ * Fractional DPRs are rounded upward to the nearest 0.5.
28
+ *
29
+ * @param {number} logicalHeight
30
+ */
31
+ export function resolveCaptureDevicePixelRatio(logicalHeight) {
32
+ if (!Number.isFinite(logicalHeight) || logicalHeight <= 0) {
33
+ throw new Error(
34
+ `Expected a positive logical height, got ${logicalHeight}.`
35
+ );
36
+ }
37
+
38
+ if (logicalHeight >= MIN_VERTICAL_RESOLUTION) {
39
+ return 1;
40
+ }
41
+
42
+ const requiredDevicePixelRatio = MIN_VERTICAL_RESOLUTION / logicalHeight;
43
+ return Math.ceil(requiredDevicePixelRatio * 2) / 2;
44
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"screenshotHarness.d.ts","sourceRoot":"","sources":["../../src/screenshotHarness.js"],"names":[],"mappings":"8BAQa;IACR,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,OAAO,CAAC;QAAE,WAAW,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC5F;+BAIS,MAAM,GAAG,OAAO,UAAU,GAAG;IAAE,qBAAqB,EAAE,eAAe,CAAA;CAAE"}
1
+ {"version":3,"file":"screenshotHarness.d.ts","sourceRoot":"","sources":["../../src/screenshotHarness.js"],"names":[],"mappings":"8BAYa;IACR,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,OAAO,CAAC;QAAE,WAAW,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC5F;+BAIS,MAAM,GAAG,OAAO,UAAU,GAAG;IAAE,qBAAqB,EAAE,eAAe,CAAA;CAAE"}
@@ -1,8 +1,12 @@
1
1
  import { embed, loadSpec } from "./index.js";
2
-
3
- const DEFAULT_CONTAINER_WIDTH = 600;
4
- const DEFAULT_CONTAINER_HEIGHT = 320;
5
- const DEFAULT_LAZY_READY_TIMEOUT_MS = 30_000;
2
+ import {
3
+ resolveCaptureDevicePixelRatio,
4
+ resolveExportSize,
5
+ } from "./screenshotExport.js";
6
+
7
+ const DEFAULT_CONTAINER_WIDTH = 500;
8
+ const DEFAULT_CONTAINER_HEIGHT = 300;
9
+ const DEFAULT_LAZY_READY_TIMEOUT_MS = 10_000;
6
10
  const READY_DELAY_MS = 100;
7
11
 
8
12
  /**
@@ -89,29 +93,41 @@ async function initializeHarness(url) {
89
93
  api.getRenderedBounds(),
90
94
  api.getLogicalCanvasSize()
91
95
  );
96
+ const devicePixelRatio = resolveCaptureDevicePixelRatio(
97
+ logicalSize.height
98
+ );
92
99
 
93
100
  screenshotWindow.__genomeSpyScreenshot = {
94
101
  status: "ready",
95
- detail: `Ready (${logicalSize.width}x${logicalSize.height}, DPR 1)`,
102
+ detail: `Ready (${logicalSize.width}x${logicalSize.height}, DPR ${formatDevicePixelRatio(
103
+ devicePixelRatio
104
+ )})`,
96
105
  error: "",
97
106
  async capture() {
98
107
  const currentSize = resolveExportSize(
99
108
  api.getRenderedBounds(),
100
109
  api.getLogicalCanvasSize()
101
110
  );
111
+ const currentDevicePixelRatio = resolveCaptureDevicePixelRatio(
112
+ currentSize.height
113
+ );
102
114
  return {
103
115
  logicalSize: currentSize,
104
116
  dataUrl: api.exportCanvas(
105
117
  currentSize.width,
106
118
  currentSize.height,
107
- 1,
119
+ currentDevicePixelRatio,
108
120
  "white"
109
121
  ),
110
122
  };
111
123
  },
112
124
  };
113
125
 
114
- setStatus(`Ready (${logicalSize.width}x${logicalSize.height}, DPR 1)`);
126
+ setStatus(
127
+ `Ready (${logicalSize.width}x${logicalSize.height}, DPR ${formatDevicePixelRatio(
128
+ devicePixelRatio
129
+ )})`
130
+ );
115
131
  } catch (error) {
116
132
  setFailure(error instanceof Error ? error.message : String(error));
117
133
  }
@@ -184,24 +200,10 @@ function parseTimeoutMs(value, fallback) {
184
200
  }
185
201
 
186
202
  /**
187
- * @param {{ width: number | undefined, height: number | undefined }} renderedBounds
188
- * @param {{ width: number, height: number }} logicalSize
203
+ * @param {number} value
189
204
  */
190
- function resolveExportSize(renderedBounds, logicalSize) {
191
- return {
192
- width:
193
- Number.isFinite(renderedBounds.width) && renderedBounds.width > 0
194
- ? Math.ceil(renderedBounds.width)
195
- : Number.isFinite(logicalSize.width) && logicalSize.width > 0
196
- ? logicalSize.width
197
- : DEFAULT_CONTAINER_WIDTH,
198
- height:
199
- Number.isFinite(renderedBounds.height) && renderedBounds.height > 0
200
- ? Math.ceil(renderedBounds.height)
201
- : Number.isFinite(logicalSize.height) && logicalSize.height > 0
202
- ? logicalSize.height
203
- : DEFAULT_CONTAINER_HEIGHT,
204
- };
205
+ function formatDevicePixelRatio(value) {
206
+ return value.toFixed(3).replace(/\.?0+$/, "");
205
207
  }
206
208
 
207
209
  /**
@@ -113,9 +113,9 @@ export interface Axis extends BaseAxis, ZIndexProps {
113
113
  format?: string;
114
114
 
115
115
  /**
116
- * A title for the axis (none by default).
116
+ * A title for the axis (none by default). Set to `null` to remove it.
117
117
  */
118
- title?: string;
118
+ title?: string | null;
119
119
 
120
120
  /**
121
121
  * The orthogonal offset in pixels by which to displace the axis from its position along the edge of the chart.