@genome-spy/core 0.14.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/index.js +224 -0
- package/dist/style.css +1 -0
- package/package.json +54 -0
- package/src/data/collector.js +178 -0
- package/src/data/collector.test.js +82 -0
- package/src/data/dataFlow.js +109 -0
- package/src/data/dataFlow.test.js +3 -0
- package/src/data/facetNode.js +17 -0
- package/src/data/flow.test.js +71 -0
- package/src/data/flowBatch.d.ts +40 -0
- package/src/data/flowNode.js +283 -0
- package/src/data/flowNode.test.js +49 -0
- package/src/data/flowOptimizer.js +117 -0
- package/src/data/flowOptimizer.test.js +192 -0
- package/src/data/flowTestUtils.js +63 -0
- package/src/data/formats/fasta.js +32 -0
- package/src/data/formats/fasta.test.js +26 -0
- package/src/data/sources/dataSource.js +22 -0
- package/src/data/sources/dataSourceFactory.js +24 -0
- package/src/data/sources/dataUtils.js +31 -0
- package/src/data/sources/dynamicCallbackSource.js +56 -0
- package/src/data/sources/dynamicSource.js +36 -0
- package/src/data/sources/inlineSource.js +69 -0
- package/src/data/sources/inlineSource.test.js +55 -0
- package/src/data/sources/namedSource.js +74 -0
- package/src/data/sources/sequenceSource.js +46 -0
- package/src/data/sources/sequenceSource.test.js +45 -0
- package/src/data/sources/urlSource.js +74 -0
- package/src/data/transforms/aggregate.js +69 -0
- package/src/data/transforms/clone.js +40 -0
- package/src/data/transforms/clone.test.js +10 -0
- package/src/data/transforms/coverage.js +187 -0
- package/src/data/transforms/coverage.test.js +122 -0
- package/src/data/transforms/filter.js +37 -0
- package/src/data/transforms/filter.test.js +17 -0
- package/src/data/transforms/filterScoredLabels.js +134 -0
- package/src/data/transforms/flattenCompressedExons.js +57 -0
- package/src/data/transforms/flattenDelimited.js +68 -0
- package/src/data/transforms/flattenDelimited.test.js +86 -0
- package/src/data/transforms/flattenSequence.js +39 -0
- package/src/data/transforms/flattenSequence.test.js +33 -0
- package/src/data/transforms/formula.js +39 -0
- package/src/data/transforms/formula.test.js +18 -0
- package/src/data/transforms/identifier.js +108 -0
- package/src/data/transforms/identifier.test.js +82 -0
- package/src/data/transforms/linearizeGenomicCoordinate.js +101 -0
- package/src/data/transforms/measureText.js +44 -0
- package/src/data/transforms/pileup.js +128 -0
- package/src/data/transforms/pileup.test.js +69 -0
- package/src/data/transforms/project.js +41 -0
- package/src/data/transforms/project.test.js +31 -0
- package/src/data/transforms/regexExtract.js +61 -0
- package/src/data/transforms/regexExtract.test.js +66 -0
- package/src/data/transforms/regexFold.js +141 -0
- package/src/data/transforms/regexFold.test.js +159 -0
- package/src/data/transforms/sample.js +101 -0
- package/src/data/transforms/sample.test.js +37 -0
- package/src/data/transforms/stack.js +137 -0
- package/src/data/transforms/stack.test.js +90 -0
- package/src/data/transforms/transformFactory.js +60 -0
- package/src/encoder/accessor.js +82 -0
- package/src/encoder/accessor.test.js +46 -0
- package/src/encoder/encoder.js +369 -0
- package/src/encoder/encoder.test.js +97 -0
- package/src/fonts/Lato-Regular.json +1267 -0
- package/src/fonts/Lato-Regular.png +0 -0
- package/src/fonts/OFL.txt +93 -0
- package/src/fonts/README.md +3 -0
- package/src/fonts/bmFont.d.ts +58 -0
- package/src/fonts/bmFontManager.js +357 -0
- package/src/fonts/bmFontMetrics.js +108 -0
- package/src/genome/genome.js +305 -0
- package/src/genome/genome.test.js +152 -0
- package/src/genome/genomeStore.js +54 -0
- package/src/genome/locusFormat.js +31 -0
- package/src/genome/scaleIndex.js +199 -0
- package/src/genome/scaleIndex.test.js +61 -0
- package/src/genome/scaleLocus.js +112 -0
- package/src/genome/scaleLocus.test.js +3 -0
- package/src/genomeSpy.js +753 -0
- package/src/gl/arrayBuilder.js +199 -0
- package/src/gl/dataToVertices.js +621 -0
- package/src/gl/includes/common.glsl +63 -0
- package/src/gl/includes/fp64-arithmetic.glsl +187 -0
- package/src/gl/includes/fp64-utils.js +132 -0
- package/src/gl/includes/picking.fragment.glsl +3 -0
- package/src/gl/includes/picking.vertex.glsl +29 -0
- package/src/gl/includes/sampleFacet.glsl +107 -0
- package/src/gl/includes/scales.glsl +79 -0
- package/src/gl/includes/scales_fp64.glsl +30 -0
- package/src/gl/link.fragment.glsl +18 -0
- package/src/gl/link.vertex.glsl +111 -0
- package/src/gl/point.fragment.glsl +123 -0
- package/src/gl/point.vertex.glsl +128 -0
- package/src/gl/rect.fragment.glsl +51 -0
- package/src/gl/rect.vertex.glsl +114 -0
- package/src/gl/rule.fragment.glsl +52 -0
- package/src/gl/rule.vertex.glsl +89 -0
- package/src/gl/text.fragment.glsl +31 -0
- package/src/gl/text.vertex.glsl +246 -0
- package/src/gl/webGLHelper.js +490 -0
- package/src/img/bowtie.svg +1 -0
- package/src/img/genomespy-favicon.svg +34 -0
- package/src/index.html +11 -0
- package/src/index.js +151 -0
- package/src/marks/link.js +189 -0
- package/src/marks/mark.js +867 -0
- package/src/marks/markUtils.js +109 -0
- package/src/marks/pointMark.js +279 -0
- package/src/marks/rectMark.js +236 -0
- package/src/marks/rule.js +231 -0
- package/src/marks/text.js +274 -0
- package/src/options.d.ts +9 -0
- package/src/scale/colorUtils.js +184 -0
- package/src/scale/glslScaleGenerator.js +462 -0
- package/src/scale/scale.js +441 -0
- package/src/scale/scale.test.js +323 -0
- package/src/scale/ticks.js +198 -0
- package/src/scale/ticks.test.js +39 -0
- package/src/singlePageApp.js +13 -0
- package/src/spec/axis.d.ts +296 -0
- package/src/spec/channel.d.ts +127 -0
- package/src/spec/data.d.ts +185 -0
- package/src/spec/font.d.ts +15 -0
- package/src/spec/genome.d.ts +35 -0
- package/src/spec/mark.d.ts +432 -0
- package/src/spec/root.d.ts +22 -0
- package/src/spec/scale.d.ts +265 -0
- package/src/spec/tooltip.d.ts +9 -0
- package/src/spec/transform.d.ts +479 -0
- package/src/spec/view.d.ts +215 -0
- package/src/styles/genome-spy.scss +153 -0
- package/src/tooltip/dataTooltipHandler.js +59 -0
- package/src/tooltip/refseqGeneTooltipHandler.js +77 -0
- package/src/tooltip/tooltipHandler.ts +12 -0
- package/src/types/filetypes.d.ts +4 -0
- package/src/types/flatqueue.d.ts +53 -0
- package/src/types/glsl.d.ts +4 -0
- package/src/types/object.d.ts +21 -0
- package/src/types/vega-scale.d.ts +60 -0
- package/src/utils/animator.js +83 -0
- package/src/utils/arrayUtils.js +55 -0
- package/src/utils/binnedRangeIndex.js +83 -0
- package/src/utils/clamp.js +8 -0
- package/src/utils/cloner.js +32 -0
- package/src/utils/cloner.test.js +23 -0
- package/src/utils/coalesce.js +11 -0
- package/src/utils/coalesce.test.js +15 -0
- package/src/utils/concatIterables.js +26 -0
- package/src/utils/concatIterables.test.js +7 -0
- package/src/utils/debounce.js +37 -0
- package/src/utils/domainArray.js +224 -0
- package/src/utils/domainArray.test.js +129 -0
- package/src/utils/eerp.js +13 -0
- package/src/utils/expression.js +32 -0
- package/src/utils/field.js +28 -0
- package/src/utils/fisheye.js +60 -0
- package/src/utils/formatObject.js +31 -0
- package/src/utils/html.js +23 -0
- package/src/utils/html.test.js +13 -0
- package/src/utils/indexer.js +43 -0
- package/src/utils/indexer.test.js +46 -0
- package/src/utils/inertia.js +124 -0
- package/src/utils/interactionEvent.js +33 -0
- package/src/utils/iterateNestedMaps.js +21 -0
- package/src/utils/iterateNestedMaps.test.js +32 -0
- package/src/utils/kWayMerge.js +42 -0
- package/src/utils/kWayMerge.test.js +25 -0
- package/src/utils/layout/flexLayout.js +336 -0
- package/src/utils/layout/flexLayout.test.js +296 -0
- package/src/utils/layout/padding.js +107 -0
- package/src/utils/layout/point.js +23 -0
- package/src/utils/layout/rectangle.js +282 -0
- package/src/utils/layout/rectangle.test.js +171 -0
- package/src/utils/mergeObjects.js +99 -0
- package/src/utils/mergeObjects.test.js +41 -0
- package/src/utils/numberExtractor.js +24 -0
- package/src/utils/numberExtractor.test.js +5 -0
- package/src/utils/point.js +14 -0
- package/src/utils/propertyCacher.js +70 -0
- package/src/utils/propertyCacher.test.js +84 -0
- package/src/utils/propertyCoalescer.js +37 -0
- package/src/utils/propertyCoalescer.test.js +21 -0
- package/src/utils/reservationMap.js +103 -0
- package/src/utils/reservationMap.test.js +19 -0
- package/src/utils/scaleNull.js +19 -0
- package/src/utils/setOperations.js +75 -0
- package/src/utils/smoothstep.js +10 -0
- package/src/utils/throttle.js +34 -0
- package/src/utils/topK.js +76 -0
- package/src/utils/topK.test.js +63 -0
- package/src/utils/transition.js +74 -0
- package/src/utils/ui/tooltip.js +189 -0
- package/src/utils/url.js +22 -0
- package/src/utils/variableTools.js +24 -0
- package/src/utils/variableTools.test.js +12 -0
- package/src/view/axisResolution.js +135 -0
- package/src/view/axisResolution.test.js +200 -0
- package/src/view/axisView.js +746 -0
- package/src/view/channel.js +5 -0
- package/src/view/concatView.js +296 -0
- package/src/view/containerView.js +141 -0
- package/src/view/decoratorView.js +510 -0
- package/src/view/facetView.js +488 -0
- package/src/view/flowBuilder.js +362 -0
- package/src/view/flowBuilder.test.js +124 -0
- package/src/view/importView.js +19 -0
- package/src/view/layerView.js +60 -0
- package/src/view/rendering.d.ts +44 -0
- package/src/view/renderingContext/compositeViewRenderingContext.js +51 -0
- package/src/view/renderingContext/deferredViewRenderingContext.js +174 -0
- package/src/view/renderingContext/layoutRecorderViewRenderingContext.js +128 -0
- package/src/view/renderingContext/simpleViewRenderingContext.js +62 -0
- package/src/view/renderingContext/svgViewRenderingContext.js +121 -0
- package/src/view/renderingContext/viewRenderingContext.js +41 -0
- package/src/view/scaleResolution.js +756 -0
- package/src/view/scaleResolution.test.js +571 -0
- package/src/view/scaleResolutionApi.d.ts +40 -0
- package/src/view/testUtils.js +48 -0
- package/src/view/unitView.js +368 -0
- package/src/view/view.js +589 -0
- package/src/view/view.test.js +213 -0
- package/src/view/viewContext.d.ts +57 -0
- package/src/view/viewFactory.js +179 -0
- package/src/view/viewFactory.test.js +16 -0
- package/src/view/viewUtils.js +420 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { range } from "d3-array";
|
|
2
|
+
import { topK, topKSlice } from "./topK";
|
|
3
|
+
|
|
4
|
+
test("topK returns top k numbers in priority order", () => {
|
|
5
|
+
/** @param {number} x */
|
|
6
|
+
const priorityAccessor = (x) => x;
|
|
7
|
+
|
|
8
|
+
expect(topK([1, 2, 3], 3, priorityAccessor)).toEqual([3, 2, 1]);
|
|
9
|
+
expect(topK([1, 2, 3], 1, priorityAccessor)).toEqual([3]);
|
|
10
|
+
expect(topK([1, 2, 3], 6, priorityAccessor)).toEqual([3, 2, 1]);
|
|
11
|
+
expect(topK([1, 2, 3, 4, 5, 6], 3, priorityAccessor)).toEqual([6, 5, 4]);
|
|
12
|
+
expect(topK([0, 9, 1, 8, 2, 7, 3, 6, 4, 5], 3, priorityAccessor)).toEqual([
|
|
13
|
+
9, 8, 7,
|
|
14
|
+
]);
|
|
15
|
+
expect(topK([1, 1, 1], 3, priorityAccessor)).toEqual([1, 1, 1]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("topK returns top k objects in priority order", () => {
|
|
19
|
+
/** @param {{priority: number}} d */
|
|
20
|
+
const priorityAccessor = (d) => d.priority;
|
|
21
|
+
|
|
22
|
+
expect(
|
|
23
|
+
topK(
|
|
24
|
+
[0, 9, 1, 8, 2, 7, 3, 6, 4, 5].map((x) => ({ priority: x })),
|
|
25
|
+
3,
|
|
26
|
+
priorityAccessor
|
|
27
|
+
)
|
|
28
|
+
).toEqual([9, 8, 7].map((x) => ({ priority: x })));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("topK returns top k objects in priority order with large datasets", () => {
|
|
32
|
+
/** @param {number} x */
|
|
33
|
+
const priorityAccessor = (x) => x;
|
|
34
|
+
|
|
35
|
+
const n = 10000;
|
|
36
|
+
const bigArray = range(n).map((x) => Math.floor(Math.random() * 100));
|
|
37
|
+
const sortedBigArray = bigArray.slice().sort((a, b) => b - a);
|
|
38
|
+
|
|
39
|
+
for (let k = 0; k < 13000; k += 1000) {
|
|
40
|
+
expect(topK(bigArray, k, priorityAccessor)).toEqual(
|
|
41
|
+
sortedBigArray.slice(0, k)
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("topKSlice returns top k indexes in priority order", () => {
|
|
47
|
+
expect(topKSlice([0, 1, 2], 3)).toEqual([2, 1, 0]);
|
|
48
|
+
expect(topKSlice([1, 2, 3], 3)).toEqual([2, 1, 0]);
|
|
49
|
+
expect(topKSlice([0, 1, 2], 1)).toEqual([2]);
|
|
50
|
+
expect(topKSlice([0, 1, 2], 6)).toEqual([2, 1, 0]);
|
|
51
|
+
expect(topKSlice([0, 1, 2, 3, 4, 5], 3)).toEqual([5, 4, 3]);
|
|
52
|
+
expect(topKSlice([0, 9, 1, 8, 2, 7, 3, 6, 4, 5], 3)).toEqual([1, 3, 5]);
|
|
53
|
+
expect(new Set(topKSlice([1, 1, 1, 2, 2, 2], 3))).toEqual(
|
|
54
|
+
new Set([3, 4, 5])
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("topKSlice returns top k indexes from a slice in priority order", () => {
|
|
59
|
+
expect(topKSlice([0, 1, 2, 3, 4, 5], 2, 1, 5)).toEqual([4, 3]);
|
|
60
|
+
expect(topKSlice([0, 9, 1, 8, 2, 7, 3, 6, 4, 5], 3, 1, 5)).toEqual([
|
|
61
|
+
1, 3, 4,
|
|
62
|
+
]);
|
|
63
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** @param {number} ms */
|
|
2
|
+
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {TransitionOptions} options
|
|
6
|
+
*
|
|
7
|
+
* @typedef {Object} TransitionOptions
|
|
8
|
+
* @prop {number} [from] default: 0
|
|
9
|
+
* @prop {number} [to] default: 1
|
|
10
|
+
* @prop {number} [duration] in milliseconds, default: 1000
|
|
11
|
+
* @prop {number} [delay] milliseconds to wait before the transition starts, default: 0
|
|
12
|
+
* @prop {function(number):void} onUpdate
|
|
13
|
+
* @prop {function(number):number} [easingFunction] default: linear
|
|
14
|
+
* @prop {function(function(number):void):void} [requestAnimationFrame]
|
|
15
|
+
* default: window.requestAnimationFrame
|
|
16
|
+
* @prop {AbortSignal} [signal]
|
|
17
|
+
*/
|
|
18
|
+
export default function transition(options) {
|
|
19
|
+
const requestAnimationFrame =
|
|
20
|
+
options.requestAnimationFrame || window.requestAnimationFrame;
|
|
21
|
+
|
|
22
|
+
const signal = options.signal;
|
|
23
|
+
|
|
24
|
+
const makePromise = () =>
|
|
25
|
+
new Promise((resolve, reject) => {
|
|
26
|
+
if (signal?.aborted) {
|
|
27
|
+
return reject("aborted");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const beginTimestamp = performance.now();
|
|
31
|
+
const endTimestamp = beginTimestamp + (options.duration || 1000);
|
|
32
|
+
|
|
33
|
+
const from = typeof options.from == "number" ? options.from : 0;
|
|
34
|
+
const to = typeof options.to == "number" ? options.to : 1;
|
|
35
|
+
const ease = options.easingFunction || ((x) => x);
|
|
36
|
+
|
|
37
|
+
/** @param {number} x */
|
|
38
|
+
const toUnit = (x) =>
|
|
39
|
+
(x - beginTimestamp) / (endTimestamp - beginTimestamp);
|
|
40
|
+
|
|
41
|
+
/** @param {number} x */
|
|
42
|
+
const toRange = (x) => x * (to - from) + from;
|
|
43
|
+
|
|
44
|
+
/** @param {number} x */
|
|
45
|
+
const clamp = (x) => Math.max(0, Math.min(1, x));
|
|
46
|
+
|
|
47
|
+
/** @param {number} stamp */
|
|
48
|
+
const step = (stamp) => {
|
|
49
|
+
if (signal?.aborted) {
|
|
50
|
+
reject("aborted");
|
|
51
|
+
} else {
|
|
52
|
+
options.onUpdate(toRange(ease(clamp(toUnit(stamp)))));
|
|
53
|
+
if (stamp < endTimestamp) {
|
|
54
|
+
requestAnimationFrame(step);
|
|
55
|
+
} else {
|
|
56
|
+
options.onUpdate(toRange(ease(1)));
|
|
57
|
+
resolve();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
requestAnimationFrame(step);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (options.delay) {
|
|
66
|
+
if (signal?.aborted) {
|
|
67
|
+
return Promise.reject("aborted");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return wait(options.delay).then(makePromise);
|
|
71
|
+
} else {
|
|
72
|
+
return makePromise();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import clientPoint from "../point";
|
|
2
|
+
import { html, render } from "lit-html";
|
|
3
|
+
import { peek } from "../arrayUtils";
|
|
4
|
+
|
|
5
|
+
export const SUPPRESS_TOOLTIP_CLASS_NAME = "gs-suppress-tooltip";
|
|
6
|
+
|
|
7
|
+
export default class Tooltip {
|
|
8
|
+
/**
|
|
9
|
+
* @param {HTMLElement} container
|
|
10
|
+
*/
|
|
11
|
+
constructor(container) {
|
|
12
|
+
this.container = container;
|
|
13
|
+
|
|
14
|
+
this.element = document.createElement("div");
|
|
15
|
+
this.element.className = "tooltip";
|
|
16
|
+
this._visible = true;
|
|
17
|
+
this.container.appendChild(this.element);
|
|
18
|
+
|
|
19
|
+
/** @type {any} */
|
|
20
|
+
this._previousTooltipDatum = undefined;
|
|
21
|
+
|
|
22
|
+
this.enabledStack = [true];
|
|
23
|
+
|
|
24
|
+
this._penaltyUntil = 0;
|
|
25
|
+
/** @type {[number, number]} */
|
|
26
|
+
this._lastCoords = undefined;
|
|
27
|
+
|
|
28
|
+
this._previousMove = 0;
|
|
29
|
+
|
|
30
|
+
this.clear();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {boolean} visible
|
|
35
|
+
*/
|
|
36
|
+
set visible(visible) {
|
|
37
|
+
if (visible != this._visible) {
|
|
38
|
+
this.element.style.display = visible ? null : "none";
|
|
39
|
+
this._visible = visible;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get visible() {
|
|
44
|
+
return this._visible;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get enabled() {
|
|
48
|
+
return peek(this.enabledStack) ?? true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {boolean} enabled True if tooltip is enabled (allowed to be shown)
|
|
53
|
+
*/
|
|
54
|
+
pushEnabledState(enabled) {
|
|
55
|
+
this.enabledStack.push(enabled);
|
|
56
|
+
if (!enabled) {
|
|
57
|
+
this.visible = false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
popEnabledState() {
|
|
62
|
+
this.enabledStack.pop();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {MouseEvent} mouseEvent
|
|
67
|
+
*/
|
|
68
|
+
handleMouseMove(mouseEvent) {
|
|
69
|
+
this.mouseCoords = clientPoint(this.container, mouseEvent);
|
|
70
|
+
|
|
71
|
+
const now = performance.now();
|
|
72
|
+
|
|
73
|
+
// Prevent the tooltip from flashing briefly before it becomes penalized
|
|
74
|
+
// because of a quickly moving mouse pointer
|
|
75
|
+
if (
|
|
76
|
+
!this.visible &&
|
|
77
|
+
!this._isPenalty() &&
|
|
78
|
+
now - this._previousMove > 500
|
|
79
|
+
) {
|
|
80
|
+
this._penaltyUntil = now + 70;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Disable the tooltip for a while if the mouse is being moved very quickly.
|
|
84
|
+
// Makes the tooltip less annoying.
|
|
85
|
+
// TODO: Should calculate speed: pixels per millisecond or something
|
|
86
|
+
if (
|
|
87
|
+
this._lastCoords &&
|
|
88
|
+
distance(this.mouseCoords, this._lastCoords) > 20
|
|
89
|
+
) {
|
|
90
|
+
this._penaltyUntil = now + 400;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this._lastCoords = this.mouseCoords;
|
|
94
|
+
|
|
95
|
+
if (this.visible) {
|
|
96
|
+
this.updatePlacement();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
this._previousMove = now;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
updatePlacement() {
|
|
103
|
+
/** Space between pointer and tooltip box */
|
|
104
|
+
const spacing = 20;
|
|
105
|
+
|
|
106
|
+
const [mouseX, mouseY] = this.mouseCoords;
|
|
107
|
+
|
|
108
|
+
let x = mouseX + spacing;
|
|
109
|
+
if (x > this.container.clientWidth - this.element.offsetWidth) {
|
|
110
|
+
x = mouseX - spacing - this.element.offsetWidth;
|
|
111
|
+
}
|
|
112
|
+
this.element.style.left = x + "px";
|
|
113
|
+
|
|
114
|
+
this.element.style.top =
|
|
115
|
+
Math.min(
|
|
116
|
+
mouseY + spacing,
|
|
117
|
+
this.container.clientHeight - this.element.offsetHeight
|
|
118
|
+
) + "px";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @param {string | import("lit").TemplateResult | HTMLElement} content
|
|
123
|
+
*/
|
|
124
|
+
setContent(content) {
|
|
125
|
+
if (!content || !this.enabled || this._isPenalty()) {
|
|
126
|
+
if (this.visible) {
|
|
127
|
+
render("", this.element);
|
|
128
|
+
this.visible = false;
|
|
129
|
+
}
|
|
130
|
+
this._previousTooltipDatum = undefined;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
render(content, this.element);
|
|
135
|
+
|
|
136
|
+
this.visible = true;
|
|
137
|
+
|
|
138
|
+
this.updatePlacement();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
clear() {
|
|
142
|
+
this._previousTooltipDatum = undefined;
|
|
143
|
+
this.setContent(undefined);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Updates the tooltip if the provided datum differs from the previous one.
|
|
148
|
+
* Otherwise this is nop.
|
|
149
|
+
*
|
|
150
|
+
* @param {T} datum
|
|
151
|
+
* @param {function(T):Promise<string | HTMLElement | import("lit").TemplateResult>} [converter]
|
|
152
|
+
* @template T
|
|
153
|
+
*/
|
|
154
|
+
updateWithDatum(datum, converter) {
|
|
155
|
+
if (datum !== this._previousTooltipDatum) {
|
|
156
|
+
this._previousTooltipDatum = datum;
|
|
157
|
+
if (!converter) {
|
|
158
|
+
converter = (d) =>
|
|
159
|
+
Promise.resolve(html` ${JSON.stringify(d)} `);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
converter(datum)
|
|
163
|
+
.then((result) => this.setContent(result))
|
|
164
|
+
.catch((error) => {
|
|
165
|
+
if (error !== "debounced") {
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
_isPenalty() {
|
|
173
|
+
return this._penaltyUntil && this._penaltyUntil > performance.now();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Calculate euclidean distance
|
|
179
|
+
*
|
|
180
|
+
* @param {number[]} a
|
|
181
|
+
* @param {number[]} b
|
|
182
|
+
*/
|
|
183
|
+
function distance(a, b) {
|
|
184
|
+
let sum = 0;
|
|
185
|
+
for (let i = 0; i < a.length; i++) {
|
|
186
|
+
sum += (a[i] - b[i]) ** 2;
|
|
187
|
+
}
|
|
188
|
+
return Math.sqrt(sum);
|
|
189
|
+
}
|
package/src/utils/url.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const protoRe = /^([A-Za-z]+:)?\/\//;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Append a relative or absolute url to a base url.
|
|
5
|
+
* The base part is omitted if the append part is absolute.
|
|
6
|
+
*
|
|
7
|
+
* @param {function():string} baseAccessor
|
|
8
|
+
* @param {string} append
|
|
9
|
+
*/
|
|
10
|
+
export function appendToBaseUrl(baseAccessor, append) {
|
|
11
|
+
if (append && protoRe.test(append)) {
|
|
12
|
+
return append;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const base = baseAccessor();
|
|
16
|
+
|
|
17
|
+
if (base && append) {
|
|
18
|
+
return base.endsWith("/") ? base + append : base + "/" + append;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return base ?? append;
|
|
22
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { isBoolean, isNumber, isString } from "vega-util";
|
|
2
|
+
|
|
3
|
+
/** A set of typical NA values */
|
|
4
|
+
export const NAs = new Set(["", "NA", ".", "-"]);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Tests whether the given array of strings can be interpreted as a numeric vector
|
|
8
|
+
*
|
|
9
|
+
* @param {string[]} values
|
|
10
|
+
*/
|
|
11
|
+
export function inferNumeric(values) {
|
|
12
|
+
return values
|
|
13
|
+
.filter((value) => typeof value == "string")
|
|
14
|
+
.filter((value) => !NAs.has(value))
|
|
15
|
+
.every((value) => /^[+-]?\d+(\.\d*)?$/.test(value));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {any} value
|
|
20
|
+
* @returns {value is string | number | boolean}
|
|
21
|
+
*/
|
|
22
|
+
export function isScalar(value) {
|
|
23
|
+
return isString(value) || isNumber(value) || isBoolean(value);
|
|
24
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as vt from "./variableTools";
|
|
2
|
+
|
|
3
|
+
test("InferNumerality", () => {
|
|
4
|
+
expect(vt.inferNumeric(["0", "1", "2.2", "-4"])).toBeTruthy();
|
|
5
|
+
expect(vt.inferNumeric(["0", ...vt.NAs.values()])).toBeTruthy();
|
|
6
|
+
expect(vt.inferNumeric([])).toBeTruthy();
|
|
7
|
+
|
|
8
|
+
expect(vt.inferNumeric(["0", "x"])).toBeFalsy();
|
|
9
|
+
expect(vt.inferNumeric(["0", " "])).toBeFalsy();
|
|
10
|
+
expect(vt.inferNumeric(["0", "1,2"])).toBeFalsy();
|
|
11
|
+
expect(vt.inferNumeric(["0", "20x"])).toBeFalsy();
|
|
12
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { isString } from "vega-util";
|
|
2
|
+
import {
|
|
3
|
+
getChannelDefWithScale,
|
|
4
|
+
getPrimaryChannel,
|
|
5
|
+
isExprDef,
|
|
6
|
+
isFieldDef,
|
|
7
|
+
isSecondaryChannel,
|
|
8
|
+
} from "../encoder/encoder";
|
|
9
|
+
import { peek } from "../utils/arrayUtils";
|
|
10
|
+
import coalesce from "../utils/coalesce";
|
|
11
|
+
|
|
12
|
+
import mergeObjects from "../utils/mergeObjects";
|
|
13
|
+
import { getCachedOrCall } from "../utils/propertyCacher";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
*
|
|
17
|
+
* @typedef { import("./unitView").default} UnitView
|
|
18
|
+
*/
|
|
19
|
+
export default class AxisResolution {
|
|
20
|
+
/**
|
|
21
|
+
* @param {import("../spec/channel").Channel} channel
|
|
22
|
+
*/
|
|
23
|
+
constructor(channel) {
|
|
24
|
+
this.channel = channel;
|
|
25
|
+
/** @type {import("./scaleResolution").ResolutionMember[]} The involved views */
|
|
26
|
+
this.members = [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get scaleResolution() {
|
|
30
|
+
return peek(this.members)?.view.getScaleResolution(this.channel);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* N.B. This is expected to be called in depth-first order, AFTER the
|
|
35
|
+
* scales have been resolved.
|
|
36
|
+
*
|
|
37
|
+
* @param {UnitView} view
|
|
38
|
+
* @param {import("../spec/channel").Channel} channel TODO: Do something for this
|
|
39
|
+
*/
|
|
40
|
+
pushUnitView(view, channel) {
|
|
41
|
+
const newScaleResolution = view.getScaleResolution(this.channel);
|
|
42
|
+
|
|
43
|
+
if (!newScaleResolution) {
|
|
44
|
+
throw new Error("Cannot find a scale resolution!");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (
|
|
48
|
+
this.scaleResolution &&
|
|
49
|
+
newScaleResolution !== this.scaleResolution
|
|
50
|
+
) {
|
|
51
|
+
throw new Error("Shared axes must have a shared scale!");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.members.push({ view, channel });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getAxisProps() {
|
|
58
|
+
return getCachedOrCall(this, "axisProps", () => {
|
|
59
|
+
const propArray = this.members.map(
|
|
60
|
+
(member) =>
|
|
61
|
+
getChannelDefWithScale(member.view, member.channel).axis
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (
|
|
65
|
+
propArray.length > 0 &&
|
|
66
|
+
propArray.some((props) => props === null)
|
|
67
|
+
) {
|
|
68
|
+
// No axis whatsoever is wanted
|
|
69
|
+
return null;
|
|
70
|
+
} else {
|
|
71
|
+
return /** @type { import("../spec/axis").Axis} */ (
|
|
72
|
+
mergeObjects(
|
|
73
|
+
propArray.filter((props) => props !== undefined),
|
|
74
|
+
"axis",
|
|
75
|
+
["title"]
|
|
76
|
+
)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getTitle() {
|
|
83
|
+
/** @param {import("./scaleResolution").ResolutionMember} member} */
|
|
84
|
+
const computeTitle = (member) => {
|
|
85
|
+
const channelDef = getChannelDefWithScale(
|
|
86
|
+
member.view,
|
|
87
|
+
member.channel
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Retain nulls as they indicate that no title should be shown
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
member,
|
|
94
|
+
explicitTitle: coalesce(
|
|
95
|
+
channelDef.axis?.title,
|
|
96
|
+
channelDef.title
|
|
97
|
+
),
|
|
98
|
+
implicitTitle: coalesce(
|
|
99
|
+
isFieldDef(channelDef) ? channelDef.field : undefined,
|
|
100
|
+
isExprDef(channelDef) ? channelDef.expr : undefined
|
|
101
|
+
),
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const titles = this.members.map(computeTitle);
|
|
106
|
+
|
|
107
|
+
// Skip implicit secondary channel titles if the primary channel has an explicit title
|
|
108
|
+
const filteredTitles = titles.filter((title) => {
|
|
109
|
+
if (
|
|
110
|
+
isSecondaryChannel(title.member.channel) &&
|
|
111
|
+
!title.explicitTitle
|
|
112
|
+
) {
|
|
113
|
+
const primaryChannel = getPrimaryChannel(title.member.channel);
|
|
114
|
+
return (
|
|
115
|
+
titles.find(
|
|
116
|
+
(title2) =>
|
|
117
|
+
title2.member.view == title.member.view &&
|
|
118
|
+
title2.member.channel == primaryChannel
|
|
119
|
+
)?.explicitTitle === undefined
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const uniqueTitles = new Set(
|
|
126
|
+
filteredTitles
|
|
127
|
+
.map((title) =>
|
|
128
|
+
coalesce(title.explicitTitle, title.implicitTitle)
|
|
129
|
+
)
|
|
130
|
+
.filter(isString)
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
return uniqueTitles.size ? [...uniqueTitles].join(", ") : null;
|
|
134
|
+
}
|
|
135
|
+
}
|