@genome-spy/core 0.19.0 → 0.21.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 +46 -119
- package/dist/schema.json +213 -25
- package/package.json +4 -3
- package/src/data/collector.test.js +2 -0
- package/src/data/dataFlow.test.js +2 -0
- package/src/data/flow.test.js +1 -0
- package/src/data/flowNode.test.js +1 -0
- package/src/data/flowOptimizer.test.js +1 -0
- package/src/data/formats/fasta.test.js +1 -0
- package/src/data/sources/inlineSource.test.js +1 -0
- package/src/data/sources/sequenceSource.test.js +1 -0
- package/src/data/transforms/clone.test.js +1 -0
- package/src/data/transforms/coverage.test.js +1 -0
- package/src/data/transforms/filter.test.js +1 -0
- package/src/data/transforms/flattenDelimited.test.js +1 -0
- package/src/data/transforms/flattenSequence.test.js +1 -0
- package/src/data/transforms/formula.test.js +1 -0
- package/src/data/transforms/identifier.test.js +1 -0
- package/src/data/transforms/pileup.test.js +1 -0
- package/src/data/transforms/project.test.js +1 -0
- package/src/data/transforms/regexExtract.test.js +1 -0
- package/src/data/transforms/regexFold.test.js +1 -0
- package/src/data/transforms/sample.test.js +1 -0
- package/src/data/transforms/stack.test.js +1 -0
- package/src/encoder/accessor.test.js +1 -0
- package/src/encoder/encoder.test.js +1 -0
- package/src/genome/genome.test.js +1 -0
- package/src/genome/scaleIndex.js +3 -2
- package/src/genome/scaleIndex.test.js +23 -6
- package/src/genome/scaleLocus.test.js +1 -0
- package/src/genomeSpy.js +16 -11
- package/src/gl/dataToVertices.js +52 -52
- package/src/gl/includes/common.glsl +12 -12
- package/src/gl/includes/picking.fragment.glsl +0 -2
- package/src/gl/includes/picking.vertex.glsl +0 -2
- package/src/gl/includes/scales.glsl +33 -2
- package/src/gl/point.vertex.glsl +0 -2
- package/src/gl/rule.vertex.glsl +1 -1
- package/src/gl/webGLHelper.js +0 -3
- package/src/marks/link.js +32 -39
- package/src/marks/mark.js +176 -106
- package/src/marks/pointMark.js +28 -59
- package/src/marks/rectMark.js +38 -33
- package/src/marks/rule.js +31 -21
- package/src/marks/text.js +18 -14
- package/src/scale/glslScaleGenerator.js +56 -17
- package/src/scale/scale.test.js +1 -0
- package/src/scale/ticks.test.js +1 -0
- package/src/spec/mark.d.ts +0 -3
- package/src/spec/scale.d.ts +0 -9
- package/src/spec/title.d.ts +102 -0
- package/src/spec/view.d.ts +6 -4
- package/src/tooltip/dataTooltipHandler.js +3 -2
- package/src/utils/addBaseUrl.test.js +1 -0
- package/src/utils/binnedIndex.js +147 -0
- package/src/utils/binnedIndex.test.js +73 -0
- package/src/utils/cloner.test.js +1 -0
- package/src/utils/coalesce.test.js +1 -0
- package/src/utils/concatIterables.test.js +1 -0
- package/src/utils/domainArray.test.js +1 -0
- package/src/utils/indexer.test.js +1 -0
- package/src/utils/iterateNestedMaps.test.js +1 -0
- package/src/utils/kWayMerge.test.js +1 -0
- package/src/utils/layout/flexLayout.js +35 -3
- package/src/utils/layout/flexLayout.test.js +15 -0
- package/src/utils/layout/grid.js +95 -0
- package/src/utils/layout/grid.test.js +71 -0
- package/src/utils/layout/padding.js +13 -0
- package/src/utils/layout/rectangle.js +6 -0
- package/src/utils/layout/rectangle.test.js +1 -0
- package/src/utils/mergeObjects.test.js +1 -0
- package/src/utils/numberExtractor.test.js +1 -0
- package/src/utils/propertyCacher.test.js +1 -0
- package/src/utils/propertyCoalescer.test.js +1 -0
- package/src/utils/reservationMap.test.js +1 -0
- package/src/utils/topK.test.js +1 -0
- package/src/utils/variableTools.test.js +1 -0
- package/src/view/axisResolution.test.js +1 -0
- package/src/view/axisView.js +3 -5
- package/src/view/concatView.js +24 -275
- package/src/view/flowBuilder.test.js +1 -0
- package/src/view/gridView.js +774 -0
- package/src/view/implicitRootView.js +14 -0
- package/src/view/layerView.js +15 -1
- package/src/view/renderingContext/deferredViewRenderingContext.js +3 -1
- package/src/view/renderingContext/simpleViewRenderingContext.js +3 -1
- package/src/view/scaleResolution.js +5 -11
- package/src/view/scaleResolution.test.js +1 -0
- package/src/view/title.js +165 -0
- package/src/view/unitView.js +9 -5
- package/src/view/view.js +35 -14
- package/src/view/view.test.js +1 -0
- package/src/view/viewContext.d.ts +6 -1
- package/src/view/viewFactory.test.js +1 -0
- package/src/view/viewUtils.js +1 -93
- package/src/view/zoom.js +89 -0
- package/src/gl/includes/fp64-arithmetic.glsl +0 -187
- package/src/gl/includes/fp64-utils.js +0 -142
- package/src/gl/includes/scales_fp64.glsl +0 -30
- package/src/utils/binnedRangeIndex.js +0 -83
- package/src/view/decoratorView.js +0 -513
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import GridView from "./gridView";
|
|
2
|
+
|
|
3
|
+
export default class ImplicitRootView extends GridView {
|
|
4
|
+
/**
|
|
5
|
+
* @param {import("./viewUtils").ViewContext} context
|
|
6
|
+
* @param {import("./view").default} view
|
|
7
|
+
*/
|
|
8
|
+
constructor(context, view) {
|
|
9
|
+
super({ vconcat: [] }, context, undefined, "implicitRoot", 1);
|
|
10
|
+
|
|
11
|
+
view.parent = this;
|
|
12
|
+
this.appendChild(view);
|
|
13
|
+
}
|
|
14
|
+
}
|
package/src/view/layerView.js
CHANGED
|
@@ -48,7 +48,6 @@ export default class LayerView extends ContainerView {
|
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
coords = coords.shrink(this.getPadding());
|
|
52
51
|
context.pushView(this, coords);
|
|
53
52
|
|
|
54
53
|
for (const child of this.children) {
|
|
@@ -57,4 +56,19 @@ export default class LayerView extends ContainerView {
|
|
|
57
56
|
|
|
58
57
|
context.popView(this);
|
|
59
58
|
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {import("../utils/interactionEvent").default} event
|
|
62
|
+
*/
|
|
63
|
+
propagateInteractionEvent(event) {
|
|
64
|
+
this.handleInteractionEvent(undefined, event, true);
|
|
65
|
+
if (this.children.length) {
|
|
66
|
+
// Propagate to the top layer
|
|
67
|
+
this.children.at(-1).propagateInteractionEvent(event);
|
|
68
|
+
}
|
|
69
|
+
if (event.stopped) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
this.handleInteractionEvent(undefined, event, false);
|
|
73
|
+
}
|
|
60
74
|
}
|
|
@@ -146,7 +146,9 @@ export default class DeferredViewRenderingContext extends ViewRenderingContext {
|
|
|
146
146
|
});
|
|
147
147
|
// Change program, set common uniforms (mark properties, shared domains)
|
|
148
148
|
this.batch.push(
|
|
149
|
-
|
|
149
|
+
...mark
|
|
150
|
+
.prepareRender(this.globalOptions)
|
|
151
|
+
.map((op) => ifEnabled(op))
|
|
150
152
|
);
|
|
151
153
|
|
|
152
154
|
/** @type {import("../../utils/layout/rectangle").default} */
|
|
@@ -55,7 +55,9 @@ export default class SimpleViewRenderingContext extends ViewRenderingContext {
|
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
mark.prepareRender(this.globalOptions)
|
|
58
|
+
for (const op of mark.prepareRender(this.globalOptions)) {
|
|
59
|
+
op();
|
|
60
|
+
}
|
|
59
61
|
mark.setViewport(this.coords, options.clipRect);
|
|
60
62
|
mark.render(options)();
|
|
61
63
|
}
|
|
@@ -228,11 +228,6 @@ export default class ScaleResolution {
|
|
|
228
228
|
props.domain = [props.domainMin ?? 0, props.domainMax ?? 1];
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
// Genomic coordinates need higher precision
|
|
232
|
-
if (props.type == LOCUS && !("fp64" in props)) {
|
|
233
|
-
props.fp64 = true;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
231
|
// Reverse discrete y axis
|
|
237
232
|
if (
|
|
238
233
|
this.channel == "y" &&
|
|
@@ -331,11 +326,6 @@ export default class ScaleResolution {
|
|
|
331
326
|
scale.genome(this.getGenome());
|
|
332
327
|
}
|
|
333
328
|
|
|
334
|
-
// Tag the scale and inform encoders and shaders that emulated
|
|
335
|
-
// 64bit floats should be used.
|
|
336
|
-
// N.B. the tag is lost upon scale.clone().
|
|
337
|
-
scale.fp64 = !!props.fp64;
|
|
338
|
-
|
|
339
329
|
if (isContinuous(scale.type)) {
|
|
340
330
|
this._zoomExtent = this._getZoomExtent();
|
|
341
331
|
}
|
|
@@ -405,12 +395,16 @@ export default class ScaleResolution {
|
|
|
405
395
|
let newDomain = [...oldDomain];
|
|
406
396
|
|
|
407
397
|
// @ts-expect-error
|
|
408
|
-
|
|
398
|
+
let anchor = scale.invert(scaleAnchor);
|
|
409
399
|
|
|
410
400
|
if (this.getScaleProps().reverse) {
|
|
411
401
|
pan = -pan;
|
|
412
402
|
}
|
|
413
403
|
|
|
404
|
+
if ("align" in scale) {
|
|
405
|
+
anchor += scale.align();
|
|
406
|
+
}
|
|
407
|
+
|
|
414
408
|
// TODO: symlog
|
|
415
409
|
switch (scale.type) {
|
|
416
410
|
case "linear":
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { isString } from "vega-util";
|
|
2
|
+
|
|
3
|
+
/** @type {Omit<Required<import("../spec/title").Title>, "text" | "style">} */
|
|
4
|
+
const BASE_TITLE_STYLE = {
|
|
5
|
+
anchor: "middle",
|
|
6
|
+
frame: "group",
|
|
7
|
+
offset: 10,
|
|
8
|
+
orient: "top",
|
|
9
|
+
align: undefined,
|
|
10
|
+
angle: 0,
|
|
11
|
+
baseline: "alphabetic",
|
|
12
|
+
dx: 0,
|
|
13
|
+
dy: 0,
|
|
14
|
+
color: undefined,
|
|
15
|
+
font: undefined,
|
|
16
|
+
fontSize: 12,
|
|
17
|
+
fontStyle: "normal",
|
|
18
|
+
fontWeight: "normal",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** @type {Partial<import("../spec/title").Title>} */
|
|
22
|
+
const TRACK_TITLE_STYLE = {
|
|
23
|
+
orient: "left",
|
|
24
|
+
anchor: "middle",
|
|
25
|
+
align: "right",
|
|
26
|
+
baseline: "middle",
|
|
27
|
+
angle: 0,
|
|
28
|
+
fontSize: 12,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** @type {Partial<import("../spec/title").Title>} */
|
|
32
|
+
const OVERLAY_TITLE_STYLE = {
|
|
33
|
+
orient: "top",
|
|
34
|
+
anchor: "start",
|
|
35
|
+
align: "left",
|
|
36
|
+
baseline: "top",
|
|
37
|
+
offset: -10,
|
|
38
|
+
dx: 10,
|
|
39
|
+
fontSize: 12,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** @type {Record<import("../spec/title").TitleAnchor, number>} */
|
|
43
|
+
const ANCHORS = {
|
|
44
|
+
start: 0,
|
|
45
|
+
middle: 0.5,
|
|
46
|
+
end: 1,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** @type {Record<import("../spec/title").TitleAnchor, import("../spec/font").Align>} */
|
|
50
|
+
const ANCHOR_TO_ALIGN = {
|
|
51
|
+
start: "left",
|
|
52
|
+
middle: "center",
|
|
53
|
+
end: "right",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {string | import("../spec/title").Title} title
|
|
58
|
+
* @returns {import("../spec/view").UnitSpec}
|
|
59
|
+
*/
|
|
60
|
+
export default function createTitle(title) {
|
|
61
|
+
if (!title) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** @type {import("../spec/title").Title} */
|
|
66
|
+
const titleSpec = isString(title) ? { text: title } : title;
|
|
67
|
+
|
|
68
|
+
if (!titleSpec.text || titleSpec.orient == "none") {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// TODO: Make these configurable
|
|
73
|
+
/** @type {Partial<import("../spec/title").Title>} */
|
|
74
|
+
const config =
|
|
75
|
+
{
|
|
76
|
+
"track-title": TRACK_TITLE_STYLE,
|
|
77
|
+
overlay: OVERLAY_TITLE_STYLE,
|
|
78
|
+
}[titleSpec.style] ?? {};
|
|
79
|
+
|
|
80
|
+
// TODO: frame prop
|
|
81
|
+
|
|
82
|
+
/** @type {import("../spec/title").Title} */
|
|
83
|
+
const preliminarySpec = {
|
|
84
|
+
...BASE_TITLE_STYLE,
|
|
85
|
+
...config,
|
|
86
|
+
...titleSpec,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** @type {Partial<import("../spec/title").Title>} */
|
|
90
|
+
let orientConfig = {};
|
|
91
|
+
let xy = { x: 0, y: 0 };
|
|
92
|
+
|
|
93
|
+
const anchorPos = ANCHORS[preliminarySpec.anchor ?? "middle"];
|
|
94
|
+
|
|
95
|
+
switch (preliminarySpec.orient) {
|
|
96
|
+
case "top":
|
|
97
|
+
xy = { x: anchorPos, y: 1 };
|
|
98
|
+
orientConfig = { baseline: "alphabetic", angle: 0 };
|
|
99
|
+
break;
|
|
100
|
+
case "right":
|
|
101
|
+
xy = { x: 1, y: 1 - anchorPos };
|
|
102
|
+
orientConfig = { baseline: "alphabetic", angle: 90 };
|
|
103
|
+
break;
|
|
104
|
+
case "bottom":
|
|
105
|
+
xy = { x: anchorPos, y: 0 };
|
|
106
|
+
orientConfig = { baseline: "top", angle: 0 };
|
|
107
|
+
break;
|
|
108
|
+
case "left":
|
|
109
|
+
xy = { x: 0, y: anchorPos };
|
|
110
|
+
orientConfig = { baseline: "alphabetic", angle: -90 };
|
|
111
|
+
break;
|
|
112
|
+
default:
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** @type {import("../spec/title").Title} */
|
|
116
|
+
const spec = {
|
|
117
|
+
...BASE_TITLE_STYLE,
|
|
118
|
+
...orientConfig,
|
|
119
|
+
...config,
|
|
120
|
+
...titleSpec,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const offsets = { xOffset: 0, yOffset: 0 };
|
|
124
|
+
switch (preliminarySpec.orient) {
|
|
125
|
+
case "top":
|
|
126
|
+
offsets.yOffset = -spec.offset;
|
|
127
|
+
break;
|
|
128
|
+
case "right":
|
|
129
|
+
offsets.xOffset = spec.offset;
|
|
130
|
+
break;
|
|
131
|
+
case "bottom":
|
|
132
|
+
offsets.yOffset = spec.offset;
|
|
133
|
+
break;
|
|
134
|
+
case "left":
|
|
135
|
+
offsets.xOffset = -spec.offset;
|
|
136
|
+
break;
|
|
137
|
+
default:
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
configurableVisibility: false,
|
|
142
|
+
data: { values: [{}] },
|
|
143
|
+
mark: {
|
|
144
|
+
type: "text",
|
|
145
|
+
tooltip: null,
|
|
146
|
+
clip: false,
|
|
147
|
+
|
|
148
|
+
...xy,
|
|
149
|
+
...offsets,
|
|
150
|
+
|
|
151
|
+
text: spec.text,
|
|
152
|
+
|
|
153
|
+
align: spec.align ?? ANCHOR_TO_ALIGN[spec.anchor],
|
|
154
|
+
angle: spec.angle,
|
|
155
|
+
baseline: spec.baseline,
|
|
156
|
+
dx: spec.dx,
|
|
157
|
+
dy: spec.dy,
|
|
158
|
+
color: spec.color,
|
|
159
|
+
font: spec.font,
|
|
160
|
+
size: spec.fontSize,
|
|
161
|
+
fontStyle: spec.fontStyle,
|
|
162
|
+
fontWeight: spec.fontWeight,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
package/src/view/unitView.js
CHANGED
|
@@ -40,7 +40,6 @@ export const markTypes = {
|
|
|
40
40
|
* @typedef {import("../encoder/accessor").Accessor} Accessor
|
|
41
41
|
* @typedef {import("../utils/layout/flexLayout").SizeDef} SizeDef
|
|
42
42
|
* @typedef {import("../spec/view").ResolutionTarget} ResolutionTarget
|
|
43
|
-
* @typedef {import("./decoratorView").default} DecoratorView
|
|
44
43
|
*
|
|
45
44
|
*/
|
|
46
45
|
export default class UnitView extends ContainerView {
|
|
@@ -64,13 +63,13 @@ export default class UnitView extends ContainerView {
|
|
|
64
63
|
throw new Error(`No such mark: ${this.getMarkType()}`);
|
|
65
64
|
}
|
|
66
65
|
|
|
67
|
-
/** @type {(UnitView | LayerView
|
|
66
|
+
/** @type {(UnitView | LayerView)[]} */
|
|
68
67
|
this.sampleAggregateViews = [];
|
|
69
68
|
this._initializeAggregateViews();
|
|
70
69
|
|
|
71
70
|
/**
|
|
72
71
|
* Not nice! Inconsistent when faceting!
|
|
73
|
-
* TODO: Something.
|
|
72
|
+
* TODO: Something. Maybe store only width/height
|
|
74
73
|
* @type {import("../utils/layout/rectangle").default}
|
|
75
74
|
*/
|
|
76
75
|
this.coords = undefined;
|
|
@@ -108,8 +107,6 @@ export default class UnitView extends ContainerView {
|
|
|
108
107
|
return;
|
|
109
108
|
}
|
|
110
109
|
|
|
111
|
-
coords = coords.shrink(this.getPadding());
|
|
112
|
-
|
|
113
110
|
this.coords = coords;
|
|
114
111
|
|
|
115
112
|
context.pushView(this, coords);
|
|
@@ -366,6 +363,13 @@ export default class UnitView extends ContainerView {
|
|
|
366
363
|
}
|
|
367
364
|
}
|
|
368
365
|
|
|
366
|
+
/**
|
|
367
|
+
* @param {import("../utils/interactionEvent").default} event
|
|
368
|
+
*/
|
|
369
|
+
propagateInteractionEvent(event) {
|
|
370
|
+
event.target = this;
|
|
371
|
+
}
|
|
372
|
+
|
|
369
373
|
/**
|
|
370
374
|
* @param {string} channel
|
|
371
375
|
* @param {import("./containerView").ResolutionTarget} resolutionType
|
package/src/view/view.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
initPropertyCache,
|
|
10
10
|
invalidatePrefix,
|
|
11
11
|
} from "../utils/propertyCacher";
|
|
12
|
-
import { isNumber, span } from "vega-util";
|
|
12
|
+
import { isNumber, isString, span } from "vega-util";
|
|
13
13
|
import { scaleLog } from "d3-scale";
|
|
14
14
|
import { isFieldDef, getPrimaryChannel } from "../encoder/encoder";
|
|
15
15
|
import { appendToBaseUrl } from "../utils/url";
|
|
@@ -81,12 +81,12 @@ export default class View {
|
|
|
81
81
|
this.resolutions = {
|
|
82
82
|
/**
|
|
83
83
|
* Channel-specific scale resolutions
|
|
84
|
-
* @type {Record<
|
|
84
|
+
* @type {Partial<Record<Channel, import("./scaleResolution").default>>}
|
|
85
85
|
*/
|
|
86
86
|
scale: {},
|
|
87
87
|
/**
|
|
88
88
|
* Channel-specific axis resolutions
|
|
89
|
-
* @type {Record<
|
|
89
|
+
* @type {Partial<Record<import("../spec/channel").PrimaryPositionalChannel, import("./axisResolution").default>>}
|
|
90
90
|
*/
|
|
91
91
|
axis: {},
|
|
92
92
|
};
|
|
@@ -103,6 +103,12 @@ export default class View {
|
|
|
103
103
|
|
|
104
104
|
/** @type {function(number):number} */
|
|
105
105
|
this.opacityFunction = defaultOpacityFunction;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Don't inherit encodings from parent.
|
|
109
|
+
* TODO: Make configurable through spec. Allow more fine-grained control.
|
|
110
|
+
*/
|
|
111
|
+
this.blockEncodingInheritance = false;
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
getPadding() {
|
|
@@ -112,15 +118,12 @@ export default class View {
|
|
|
112
118
|
}
|
|
113
119
|
|
|
114
120
|
/**
|
|
115
|
-
* Returns a
|
|
116
|
-
* bounding box. The padding may include the configured padding, axes,
|
|
117
|
-
* peripheral views, etc.
|
|
121
|
+
* Returns a padding that indicates how much axes and titles extend over the plot area.
|
|
118
122
|
*
|
|
119
|
-
*
|
|
120
|
-
* axes line up properly.
|
|
123
|
+
* @returns {Padding}
|
|
121
124
|
*/
|
|
122
|
-
|
|
123
|
-
return
|
|
125
|
+
getOverhang() {
|
|
126
|
+
return Padding.zero();
|
|
124
127
|
}
|
|
125
128
|
|
|
126
129
|
/**
|
|
@@ -367,12 +370,13 @@ export default class View {
|
|
|
367
370
|
* encodings. However, this does not contain any defaults or inferred/adjusted/fixed
|
|
368
371
|
* encodings. Those are available in Mark's encoding property.
|
|
369
372
|
*
|
|
370
|
-
* @param {View} [whoIsAsking] Passed to the immediate parent. Allows for
|
|
371
|
-
* selectively breaking the inheritance.
|
|
372
373
|
* @return {import("../spec/channel").Encoding}
|
|
373
374
|
*/
|
|
374
|
-
getEncoding(
|
|
375
|
-
const pe =
|
|
375
|
+
getEncoding() {
|
|
376
|
+
const pe =
|
|
377
|
+
this.parent && !this.blockEncodingInheritance
|
|
378
|
+
? this.parent.getEncoding()
|
|
379
|
+
: {};
|
|
376
380
|
const te = this.spec.encoding || {};
|
|
377
381
|
|
|
378
382
|
/** @type {import("../spec/channel").Encoding} */
|
|
@@ -490,6 +494,13 @@ export default class View {
|
|
|
490
494
|
return true;
|
|
491
495
|
}
|
|
492
496
|
|
|
497
|
+
getTitleText() {
|
|
498
|
+
const title = this.spec.title;
|
|
499
|
+
if (title) {
|
|
500
|
+
return isString(title) ? title : title.text;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
493
504
|
/**
|
|
494
505
|
* @param {any} key string
|
|
495
506
|
* @param {function(key?):T} callable A function that produces a value to be cached
|
|
@@ -525,6 +536,16 @@ export default class View {
|
|
|
525
536
|
invalidateSizeCache() {
|
|
526
537
|
this._invalidateCacheByPrefix("size/", "ancestors");
|
|
527
538
|
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Broadcasts a message to views that include the given (x, y) point.
|
|
542
|
+
* This is mainly intended for mouse events.
|
|
543
|
+
*
|
|
544
|
+
* @param {import("../utils/interactionEvent").default} event
|
|
545
|
+
*/
|
|
546
|
+
propagateInteractionEvent(event) {
|
|
547
|
+
// Subclasses must implement proper handling
|
|
548
|
+
}
|
|
528
549
|
}
|
|
529
550
|
|
|
530
551
|
/**
|
package/src/view/view.test.js
CHANGED
|
@@ -11,6 +11,11 @@ import { Datum } from "../data/flowNode";
|
|
|
11
11
|
import { ViewSpec } from "../spec/view";
|
|
12
12
|
import ContainerView from "./containerView";
|
|
13
13
|
|
|
14
|
+
export interface Hover {
|
|
15
|
+
mark: Mark;
|
|
16
|
+
datum: Datum;
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
/**
|
|
15
20
|
* ViewContext provides essential data and interfaces to View classes.
|
|
16
21
|
*/
|
|
@@ -29,7 +34,7 @@ export default interface ViewContext {
|
|
|
29
34
|
converter?: (datum: T) => Promise<TemplateResult>
|
|
30
35
|
) => void;
|
|
31
36
|
|
|
32
|
-
getCurrentHover: () =>
|
|
37
|
+
getCurrentHover: () => Hover;
|
|
33
38
|
|
|
34
39
|
/**
|
|
35
40
|
* Adds a keyboard event listener to the document. Cleanup is performed automatically
|
package/src/view/viewUtils.js
CHANGED
|
@@ -3,19 +3,11 @@ import { loader as vegaLoader } from "vega-loader";
|
|
|
3
3
|
|
|
4
4
|
import UnitView from "./unitView";
|
|
5
5
|
import ImportView from "./importView";
|
|
6
|
-
import LayerView from "./layerView";
|
|
7
|
-
import DecoratorView from "./decoratorView";
|
|
8
6
|
// eslint-disable-next-line no-unused-vars
|
|
9
7
|
import View, { VISIT_SKIP, VISIT_STOP } from "./view";
|
|
10
8
|
import { buildDataFlow } from "./flowBuilder";
|
|
11
9
|
import { optimizeDataFlow } from "../data/flowOptimizer";
|
|
12
|
-
import {
|
|
13
|
-
isFieldDef,
|
|
14
|
-
isValueDef,
|
|
15
|
-
primaryPositionalChannels,
|
|
16
|
-
} from "../encoder/encoder";
|
|
17
|
-
import ContainerView from "./containerView";
|
|
18
|
-
import { peek } from "../utils/arrayUtils";
|
|
10
|
+
import { isFieldDef, primaryPositionalChannels } from "../encoder/encoder";
|
|
19
11
|
import { rollup } from "d3-array";
|
|
20
12
|
|
|
21
13
|
/**
|
|
@@ -145,90 +137,6 @@ export function setImplicitScaleNames(root) {
|
|
|
145
137
|
}
|
|
146
138
|
}
|
|
147
139
|
|
|
148
|
-
/**
|
|
149
|
-
* @param {View} root
|
|
150
|
-
*/
|
|
151
|
-
export function addDecorators(root) {
|
|
152
|
-
let newRoot = root; // If the root is wrapped...
|
|
153
|
-
|
|
154
|
-
/** @param {ChannelDef} channelDef */
|
|
155
|
-
const hasDomain = (channelDef) => channelDef && !isValueDef(channelDef);
|
|
156
|
-
|
|
157
|
-
root.visit((view) => {
|
|
158
|
-
if (view instanceof LayerView || view instanceof UnitView) {
|
|
159
|
-
const encoding = view.getEncoding();
|
|
160
|
-
if (
|
|
161
|
-
view instanceof UnitView &&
|
|
162
|
-
!hasDomain(encoding.x) &&
|
|
163
|
-
!hasDomain(encoding.y)
|
|
164
|
-
) {
|
|
165
|
-
// Don't wrap views that have no positional channels
|
|
166
|
-
// TODO: However, in future, views with borders or backgrounds should be wrapped always
|
|
167
|
-
// TODO: Also, views with "axis: null" need no wrapping.
|
|
168
|
-
// TODO: Handle LayerViews, they may have children with positional domains
|
|
169
|
-
return VISIT_SKIP;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const originalParent = view.parent;
|
|
173
|
-
const decorator = new DecoratorView(view.context, originalParent);
|
|
174
|
-
view.parent = decorator;
|
|
175
|
-
decorator.child = view;
|
|
176
|
-
decorator.name = view.name + "_decorator";
|
|
177
|
-
|
|
178
|
-
if (originalParent) {
|
|
179
|
-
if (originalParent instanceof ContainerView) {
|
|
180
|
-
originalParent.replaceChild(view, decorator);
|
|
181
|
-
} else {
|
|
182
|
-
// The situation is likely related to summaries of SampleView and the
|
|
183
|
-
// hierarchy is inconsistent. Let's try to find the SampleView.
|
|
184
|
-
|
|
185
|
-
/** @type {view} */
|
|
186
|
-
let parent;
|
|
187
|
-
root.visit(
|
|
188
|
-
stackifyVisitor((needle, stack) => {
|
|
189
|
-
if (needle === view) {
|
|
190
|
-
parent = peek(stack);
|
|
191
|
-
return VISIT_STOP;
|
|
192
|
-
}
|
|
193
|
-
})
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
if (parent instanceof ContainerView) {
|
|
197
|
-
parent.replaceChild(view, decorator);
|
|
198
|
-
} else {
|
|
199
|
-
throw new Error(
|
|
200
|
-
"Cannot find parent while decorating: " +
|
|
201
|
-
view.getPathString()
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
decorator.resolutions = view.resolutions;
|
|
208
|
-
view.resolutions = { scale: {}, axis: {} };
|
|
209
|
-
|
|
210
|
-
decorator.spec.height = view.spec.height;
|
|
211
|
-
view.spec.height = "container";
|
|
212
|
-
|
|
213
|
-
decorator.spec.width = view.spec.width;
|
|
214
|
-
view.spec.width = "container";
|
|
215
|
-
|
|
216
|
-
decorator.spec.padding = view.spec.padding;
|
|
217
|
-
view.spec.padding = undefined;
|
|
218
|
-
|
|
219
|
-
if (view === root) {
|
|
220
|
-
newRoot = decorator;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
decorator.initialize();
|
|
224
|
-
|
|
225
|
-
return VISIT_SKIP;
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
return newRoot;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
140
|
/**
|
|
233
141
|
* @param {View} root
|
|
234
142
|
* @param {import("../data/dataFlow").default<View>} [existingFlow] Add data flow
|
package/src/view/zoom.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {object} ZoomEvent
|
|
3
|
+
* @prop {number} x
|
|
4
|
+
* @prop {number} y
|
|
5
|
+
* @prop {number} xDelta
|
|
6
|
+
* @prop {number} yDelta
|
|
7
|
+
* @prop {number} zDelta
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {import("../utils/interactionEvent").default} event
|
|
12
|
+
* @param {import("./renderingContext/layoutRecorderViewRenderingContext").Rectangle} coords The plot area
|
|
13
|
+
* @param {(zoomEvent: ZoomEvent) => void} handleZoom
|
|
14
|
+
* @param {import("./viewContext").Hover} [hover]
|
|
15
|
+
*/
|
|
16
|
+
export default function interactionToZoom(event, coords, handleZoom, hover) {
|
|
17
|
+
if (event.type == "wheel") {
|
|
18
|
+
event.uiEvent.preventDefault(); // TODO: Only if there was something zoomable
|
|
19
|
+
|
|
20
|
+
const wheelEvent = /** @type {WheelEvent} */ (event.uiEvent);
|
|
21
|
+
const wheelMultiplier = wheelEvent.deltaMode ? 120 : 1;
|
|
22
|
+
|
|
23
|
+
let { x, y } = event.point;
|
|
24
|
+
|
|
25
|
+
// Snapping to the hovered item:
|
|
26
|
+
// We find the currently hovered object and move the pointed coordinates
|
|
27
|
+
// to its center if the mark has only primary positional channels.
|
|
28
|
+
// This allows the user to rapidly zoom closer without having to
|
|
29
|
+
// continuously adjust the cursor position.
|
|
30
|
+
|
|
31
|
+
if (hover) {
|
|
32
|
+
const e = hover.mark.encoders;
|
|
33
|
+
if (e.x && !e.x2) {
|
|
34
|
+
x = +e.x(hover.datum) * coords.width + coords.x;
|
|
35
|
+
}
|
|
36
|
+
if (e.y && !e.y2) {
|
|
37
|
+
y = (1 - +e.y(hover.datum)) * coords.height + coords.y;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (Math.abs(wheelEvent.deltaX) < Math.abs(wheelEvent.deltaY)) {
|
|
42
|
+
handleZoom({
|
|
43
|
+
x,
|
|
44
|
+
y,
|
|
45
|
+
xDelta: 0,
|
|
46
|
+
yDelta: 0,
|
|
47
|
+
zDelta: (wheelEvent.deltaY * wheelMultiplier) / 300,
|
|
48
|
+
});
|
|
49
|
+
} else {
|
|
50
|
+
handleZoom({
|
|
51
|
+
x,
|
|
52
|
+
y,
|
|
53
|
+
xDelta: -wheelEvent.deltaX * wheelMultiplier,
|
|
54
|
+
yDelta: 0,
|
|
55
|
+
zDelta: 0,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
} else if (
|
|
59
|
+
event.type == "mousedown" &&
|
|
60
|
+
/** @type {MouseEvent} */ (event.uiEvent).button === 0
|
|
61
|
+
) {
|
|
62
|
+
const mouseEvent = /** @type {MouseEvent} */ (event.uiEvent);
|
|
63
|
+
mouseEvent.preventDefault();
|
|
64
|
+
|
|
65
|
+
let prevMouseEvent = mouseEvent;
|
|
66
|
+
|
|
67
|
+
const onMousemove = /** @param {MouseEvent} moveEvent */ (
|
|
68
|
+
moveEvent
|
|
69
|
+
) => {
|
|
70
|
+
handleZoom({
|
|
71
|
+
x: prevMouseEvent.clientX,
|
|
72
|
+
y: prevMouseEvent.clientY,
|
|
73
|
+
xDelta: moveEvent.clientX - prevMouseEvent.clientX,
|
|
74
|
+
yDelta: moveEvent.clientY - prevMouseEvent.clientY,
|
|
75
|
+
zDelta: 0,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
prevMouseEvent = moveEvent;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const onMouseup = /** @param {MouseEvent} upEvent */ (upEvent) => {
|
|
82
|
+
document.removeEventListener("mousemove", onMousemove);
|
|
83
|
+
document.removeEventListener("mouseup", onMouseup);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
document.addEventListener("mouseup", onMouseup, false);
|
|
87
|
+
document.addEventListener("mousemove", onMousemove, false);
|
|
88
|
+
}
|
|
89
|
+
}
|