@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
|
@@ -1,513 +0,0 @@
|
|
|
1
|
-
import ContainerView from "./containerView";
|
|
2
|
-
import AxisView from "./axisView";
|
|
3
|
-
import { getFlattenedViews } from "./viewUtils";
|
|
4
|
-
import Padding from "../utils/layout/padding";
|
|
5
|
-
import UnitView from "./unitView";
|
|
6
|
-
import { ZERO_FLEXDIMENSIONS } from "../utils/layout/flexLayout";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* @typedef {import("../spec/channel").PrimaryPositionalChannel} PositionalChannel
|
|
10
|
-
* @typedef {import("../spec/view").GeometricDimension} GeometricDimension
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/** @type {Record<PositionalChannel, AxisOrient[]>} */
|
|
14
|
-
const CHANNEL_ORIENTS = {
|
|
15
|
-
x: ["bottom", "top"],
|
|
16
|
-
y: ["left", "right"],
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* An internal view that wraps a unit or layer view and takes care of the axes.
|
|
21
|
-
*
|
|
22
|
-
* @typedef {import("../spec/view").LayerSpec} LayerSpec
|
|
23
|
-
* @typedef {import("./view").default} View
|
|
24
|
-
* @typedef {import("../spec/axis").Axis} Axis
|
|
25
|
-
* @typedef {import("../spec/axis").GenomeAxis} GenomeAxis
|
|
26
|
-
* @typedef {import("../spec/axis").AxisOrient} AxisOrient
|
|
27
|
-
*
|
|
28
|
-
* @typedef {Axis & { extent: number }} AugmentedAxis
|
|
29
|
-
*
|
|
30
|
-
* @typedef {object} ZoomEvent
|
|
31
|
-
* @prop {number} x
|
|
32
|
-
* @prop {number} y
|
|
33
|
-
* @prop {number} xDelta
|
|
34
|
-
* @prop {number} yDelta
|
|
35
|
-
* @prop {number} zDelta
|
|
36
|
-
*/
|
|
37
|
-
export default class DecoratorView extends ContainerView {
|
|
38
|
-
/**
|
|
39
|
-
* @param {import("./viewUtils").ViewContext} context
|
|
40
|
-
* @param {import("./containerView").default} parent
|
|
41
|
-
*/
|
|
42
|
-
constructor(context, parent) {
|
|
43
|
-
super({}, context, parent, "decorator");
|
|
44
|
-
|
|
45
|
-
/** @type { import("./layerView").default | import("./unitView").default } */
|
|
46
|
-
this.child = undefined;
|
|
47
|
-
|
|
48
|
-
/** @type {UnitView} */
|
|
49
|
-
this.backgroundView = undefined;
|
|
50
|
-
|
|
51
|
-
/** @type {Record<AxisOrient, AxisView>} */
|
|
52
|
-
this.axisViews = {
|
|
53
|
-
top: undefined,
|
|
54
|
-
right: undefined,
|
|
55
|
-
bottom: undefined,
|
|
56
|
-
left: undefined,
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
["mousedown", "wheel"].forEach((type) =>
|
|
60
|
-
this.addInteractionEventListener(
|
|
61
|
-
type,
|
|
62
|
-
this.handleMouseEvent.bind(this)
|
|
63
|
-
)
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Creates the axis views
|
|
69
|
-
*
|
|
70
|
-
* TODO: Perhaps views need a common initialization method?
|
|
71
|
-
*/
|
|
72
|
-
initialize() {
|
|
73
|
-
Object.entries(CHANNEL_ORIENTS).forEach(([channel, orients]) =>
|
|
74
|
-
this._initializeAxes(channel, orients)
|
|
75
|
-
);
|
|
76
|
-
this._invalidateCacheByPrefix("size/", "ancestors");
|
|
77
|
-
|
|
78
|
-
// TODO: Merge viewConfig from all descendants (when there are layers)
|
|
79
|
-
// TODO: Implement styles
|
|
80
|
-
|
|
81
|
-
const viewConfig = this.child.spec?.view;
|
|
82
|
-
if (viewConfig?.fill || viewConfig?.stroke) {
|
|
83
|
-
this.backgroundView = new UnitView(
|
|
84
|
-
createBackground(viewConfig),
|
|
85
|
-
this.context,
|
|
86
|
-
this,
|
|
87
|
-
"background"
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* @param {View} [whoIsAsking] Passed to the immediate parent. Allows for
|
|
94
|
-
* selectively breaking the inheritance.
|
|
95
|
-
*/
|
|
96
|
-
getEncoding(whoIsAsking) {
|
|
97
|
-
if (
|
|
98
|
-
Object.values(this.axisViews).find(
|
|
99
|
-
(view) => whoIsAsking === view
|
|
100
|
-
) ||
|
|
101
|
-
whoIsAsking == this.backgroundView
|
|
102
|
-
) {
|
|
103
|
-
// Prevent the axis views from inheriting any encodings
|
|
104
|
-
return {};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return super.getEncoding();
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* @param {View} [whoIsAsking] Passed to the immediate parent. Allows for
|
|
112
|
-
* selectively breaking the inheritance.
|
|
113
|
-
* @return {function(object):any}
|
|
114
|
-
*/
|
|
115
|
-
getFacetAccessor(whoIsAsking) {
|
|
116
|
-
if (whoIsAsking != this.child) {
|
|
117
|
-
// Axes have no facets
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (this.parent) {
|
|
122
|
-
return this.parent.getFacetAccessor(this);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* @returns {IterableIterator<View>}
|
|
128
|
-
*/
|
|
129
|
-
*[Symbol.iterator]() {
|
|
130
|
-
yield this.child;
|
|
131
|
-
if (this.backgroundView) {
|
|
132
|
-
yield this.backgroundView;
|
|
133
|
-
}
|
|
134
|
-
for (const view of Object.values(this.axisViews)) {
|
|
135
|
-
if (view) {
|
|
136
|
-
yield view;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
_getAxisExtents() {
|
|
142
|
-
return this._cache("size/axisExtents", () => {
|
|
143
|
-
/** @type {Record<AxisOrient, number>} */
|
|
144
|
-
// @ts-ignore
|
|
145
|
-
const paddings = {};
|
|
146
|
-
for (const view of Object.values(this.axisViews)) {
|
|
147
|
-
if (view) {
|
|
148
|
-
paddings[view.getOrient()] = view.getPerpendicularSize();
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return Padding.createFromRecord(paddings);
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
_getAxisOffsets() {
|
|
156
|
-
return this._cache("size/axisOffsets", () => {
|
|
157
|
-
/** @type {Record<AxisOrient, number>} */
|
|
158
|
-
// @ts-ignore
|
|
159
|
-
const paddings = {};
|
|
160
|
-
for (const view of Object.values(this.axisViews)) {
|
|
161
|
-
if (view) {
|
|
162
|
-
paddings[view.getOrient()] = view.axisProps.offset;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return Padding.createFromRecord(paddings);
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
getEffectivePadding() {
|
|
170
|
-
// TODO: Handle negative axis extents
|
|
171
|
-
return this._cache("size/effectivePadding", () =>
|
|
172
|
-
this.getPadding().add(this._getAxisExtents())
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
getSize() {
|
|
177
|
-
return this._cache("size/size", () =>
|
|
178
|
-
this.child.isVisible()
|
|
179
|
-
? this.getSizeFromSpec()
|
|
180
|
-
.addPadding(this.getPadding())
|
|
181
|
-
.addPadding(this.getAxisSizes())
|
|
182
|
-
: ZERO_FLEXDIMENSIONS
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Returns the amount of extra space the axes need on the plot edges.
|
|
188
|
-
* The calculation takes axis offsets into account.
|
|
189
|
-
*
|
|
190
|
-
* @returns {Padding}
|
|
191
|
-
*/
|
|
192
|
-
getAxisSizes() {
|
|
193
|
-
// TODO: Clamp negative sizes (if axes are positioned entirely onto the plots)
|
|
194
|
-
return this._cache("size/axisSizes", () =>
|
|
195
|
-
this._getAxisExtents().add(this._getAxisOffsets())
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* @param {import("./renderingContext/viewRenderingContext").default} context
|
|
201
|
-
* @param {import("../utils/layout/rectangle").default} coords
|
|
202
|
-
* @param {import("./view").RenderingOptions} [options]
|
|
203
|
-
*/
|
|
204
|
-
render(context, coords, options = {}) {
|
|
205
|
-
if (!this.isVisible() || !this.child.isVisible()) {
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
coords = coords.shrink(this.getPadding());
|
|
210
|
-
context.pushView(this, coords);
|
|
211
|
-
|
|
212
|
-
const extents = this._getAxisExtents();
|
|
213
|
-
const childCoords = coords.shrink(extents.add(this._getAxisOffsets()));
|
|
214
|
-
|
|
215
|
-
this._childCoords = childCoords;
|
|
216
|
-
|
|
217
|
-
if (this.backgroundView) {
|
|
218
|
-
this.backgroundView.render(context, childCoords, options);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
this.child.render(context, childCoords, options);
|
|
222
|
-
|
|
223
|
-
const entries = this._cache("axisViewEntries", () =>
|
|
224
|
-
Object.entries(this.axisViews).filter((e) => !!e[1])
|
|
225
|
-
);
|
|
226
|
-
|
|
227
|
-
for (const [orient, view] of entries) {
|
|
228
|
-
const props = view.axisProps;
|
|
229
|
-
|
|
230
|
-
/** @type {import("../utils/layout/rectangle").default} */
|
|
231
|
-
let axisCoords;
|
|
232
|
-
|
|
233
|
-
if (orient == "bottom") {
|
|
234
|
-
axisCoords = childCoords
|
|
235
|
-
.translate(0, childCoords.height + props.offset)
|
|
236
|
-
.modify({ height: extents.bottom });
|
|
237
|
-
} else if (orient == "top") {
|
|
238
|
-
axisCoords = childCoords
|
|
239
|
-
.translate(0, -extents.top - props.offset)
|
|
240
|
-
.modify({ height: extents.top });
|
|
241
|
-
} else if (orient == "left") {
|
|
242
|
-
axisCoords = childCoords
|
|
243
|
-
.translate(-extents.left - props.offset, 0)
|
|
244
|
-
.modify({ width: extents.left });
|
|
245
|
-
} else if (orient == "right") {
|
|
246
|
-
axisCoords = childCoords
|
|
247
|
-
.translate(childCoords.width + props.offset, 0)
|
|
248
|
-
.modify({ width: extents.right });
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Axes have no faceted data, thus, pass undefined facetId
|
|
252
|
-
view.render(context, axisCoords);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
context.popView(this);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Returns the views that should be scanned for resolutions: all view's ancestors and children.
|
|
260
|
-
* Axis views are not included.
|
|
261
|
-
*/
|
|
262
|
-
_getResolutionParticipants() {
|
|
263
|
-
return [...this.getAncestors(), ...getFlattenedViews(this.child)];
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* @param {string} channel
|
|
268
|
-
* @param {AxisOrient[]} orients
|
|
269
|
-
*/
|
|
270
|
-
_initializeAxes(channel, orients) {
|
|
271
|
-
const resolutions = this._getResolutionParticipants()
|
|
272
|
-
.map((view) => view.resolutions.axis[channel])
|
|
273
|
-
.filter((resolution) => resolution);
|
|
274
|
-
|
|
275
|
-
// First, fill the preferred slots
|
|
276
|
-
for (const r of resolutions) {
|
|
277
|
-
const axisProps = r.getAxisProps();
|
|
278
|
-
if (axisProps && axisProps.orient) {
|
|
279
|
-
if (!orients.includes(axisProps.orient)) {
|
|
280
|
-
throw new Error(
|
|
281
|
-
`Invalid axis orientation for '${channel}' channel: ${axisProps.orient}`
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
if (this.axisViews[axisProps.orient]) {
|
|
285
|
-
throw new Error(
|
|
286
|
-
`The slot for ${axisProps.orient} axis is already reserved!`
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
this.axisViews[axisProps.orient] = new AxisView(
|
|
290
|
-
{
|
|
291
|
-
...axisProps,
|
|
292
|
-
title: r.getTitle(),
|
|
293
|
-
},
|
|
294
|
-
r.scaleResolution.type,
|
|
295
|
-
this.context,
|
|
296
|
-
this
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Next, fill the slots in the preferred order
|
|
302
|
-
// eslint-disable-next-line no-labels
|
|
303
|
-
resolutionLoop: for (const r of resolutions) {
|
|
304
|
-
const axisProps = r.getAxisProps();
|
|
305
|
-
if (axisProps && !axisProps.orient) {
|
|
306
|
-
for (const slot of orients) {
|
|
307
|
-
if (!this.axisViews[slot]) {
|
|
308
|
-
axisProps.orient = /** @type {AxisOrient} */ (slot);
|
|
309
|
-
this.axisViews[slot] = new AxisView(
|
|
310
|
-
{
|
|
311
|
-
...axisProps,
|
|
312
|
-
title: r.getTitle(),
|
|
313
|
-
},
|
|
314
|
-
r.scaleResolution.type,
|
|
315
|
-
this.context,
|
|
316
|
-
this
|
|
317
|
-
);
|
|
318
|
-
// eslint-disable-next-line no-labels
|
|
319
|
-
continue resolutionLoop;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
throw new Error(
|
|
323
|
-
"No room for axes. All slots are already reserved."
|
|
324
|
-
);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* @param {import("../utils/layout/rectangle").default} coords
|
|
331
|
-
* Coordinates of the view
|
|
332
|
-
* @param {import("../utils/interactionEvent").default} event
|
|
333
|
-
*/
|
|
334
|
-
handleMouseEvent(coords, event) {
|
|
335
|
-
if (!this.isZoomable()) {
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// TODO: Extract a class or something
|
|
340
|
-
|
|
341
|
-
if (event.type == "wheel") {
|
|
342
|
-
event.uiEvent.preventDefault(); // TODO: Only if there was something to zoom
|
|
343
|
-
|
|
344
|
-
const wheelEvent = /** @type {WheelEvent} */ (event.uiEvent);
|
|
345
|
-
const wheelMultiplier = wheelEvent.deltaMode ? 120 : 1;
|
|
346
|
-
|
|
347
|
-
let { x, y } = event.point;
|
|
348
|
-
|
|
349
|
-
// Snapping to the hovered item:
|
|
350
|
-
// We find the currently hovered object and move the pointed coordinates
|
|
351
|
-
// to its center if the mark has only primary positional channels.
|
|
352
|
-
// This allows the user to rapidly zoom closer without having to
|
|
353
|
-
// continuously adjust the cursor position.
|
|
354
|
-
|
|
355
|
-
const hover = this.context.getCurrentHover();
|
|
356
|
-
if (hover) {
|
|
357
|
-
const viewCoords = coords.shrink(this.getEffectivePadding());
|
|
358
|
-
|
|
359
|
-
const e = hover.mark.encoders;
|
|
360
|
-
if (e.x && !e.x2) {
|
|
361
|
-
x = +e.x(hover.datum) * viewCoords.width + viewCoords.x;
|
|
362
|
-
}
|
|
363
|
-
if (e.y && !e.y2) {
|
|
364
|
-
y =
|
|
365
|
-
(1 - +e.y(hover.datum)) * viewCoords.height +
|
|
366
|
-
viewCoords.y;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
if (Math.abs(wheelEvent.deltaX) < Math.abs(wheelEvent.deltaY)) {
|
|
371
|
-
this._handleZoom(coords, {
|
|
372
|
-
x,
|
|
373
|
-
y,
|
|
374
|
-
xDelta: 0,
|
|
375
|
-
yDelta: 0,
|
|
376
|
-
zDelta: (wheelEvent.deltaY * wheelMultiplier) / 300,
|
|
377
|
-
});
|
|
378
|
-
} else {
|
|
379
|
-
this._handleZoom(coords, {
|
|
380
|
-
x,
|
|
381
|
-
y,
|
|
382
|
-
xDelta: -wheelEvent.deltaX * wheelMultiplier,
|
|
383
|
-
yDelta: 0,
|
|
384
|
-
zDelta: 0,
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
} else if (
|
|
388
|
-
event.type == "mousedown" &&
|
|
389
|
-
/** @type {MouseEvent} */ (event.uiEvent).button === 0
|
|
390
|
-
) {
|
|
391
|
-
const mouseEvent = /** @type {MouseEvent} */ (event.uiEvent);
|
|
392
|
-
mouseEvent.preventDefault();
|
|
393
|
-
|
|
394
|
-
let prevMouseEvent = mouseEvent;
|
|
395
|
-
|
|
396
|
-
const onMousemove = /** @param {MouseEvent} moveEvent */ (
|
|
397
|
-
moveEvent
|
|
398
|
-
) => {
|
|
399
|
-
this._handleZoom(coords, {
|
|
400
|
-
x: prevMouseEvent.clientX,
|
|
401
|
-
y: prevMouseEvent.clientY,
|
|
402
|
-
xDelta: moveEvent.clientX - prevMouseEvent.clientX,
|
|
403
|
-
yDelta: moveEvent.clientY - prevMouseEvent.clientY,
|
|
404
|
-
zDelta: 0,
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
prevMouseEvent = moveEvent;
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
const onMouseup = /** @param {MouseEvent} upEvent */ (upEvent) => {
|
|
411
|
-
document.removeEventListener("mousemove", onMousemove);
|
|
412
|
-
document.removeEventListener("mouseup", onMouseup);
|
|
413
|
-
};
|
|
414
|
-
|
|
415
|
-
document.addEventListener("mouseup", onMouseup, false);
|
|
416
|
-
document.addEventListener("mousemove", onMousemove, false);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
isZoomable() {
|
|
421
|
-
return this._cache("zoomable", () =>
|
|
422
|
-
Object.values(this._getZoomableResolutions()).some(
|
|
423
|
-
(set) => set.size
|
|
424
|
-
)
|
|
425
|
-
);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
_getZoomableResolutions() {
|
|
429
|
-
return this._cache("zoomableResolutions", () => {
|
|
430
|
-
/** @type {Record<import("../spec/channel").PrimaryPositionalChannel, Set<import("./scaleResolution").default>>} */
|
|
431
|
-
const resolutions = {
|
|
432
|
-
x: new Set(),
|
|
433
|
-
y: new Set(),
|
|
434
|
-
};
|
|
435
|
-
|
|
436
|
-
// Find all resolutions (scales) that are candidates for zooming
|
|
437
|
-
this.child.visit((v) => {
|
|
438
|
-
for (const [channel, resolutionSet] of Object.entries(
|
|
439
|
-
resolutions
|
|
440
|
-
)) {
|
|
441
|
-
const resolution = v.getScaleResolution(channel);
|
|
442
|
-
if (resolution && resolution.isZoomable()) {
|
|
443
|
-
resolutionSet.add(resolution);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
return resolutions;
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/**
|
|
453
|
-
*
|
|
454
|
-
* @param {import("../utils/layout/rectangle").default} coords Coordinates
|
|
455
|
-
* @param {ZoomEvent} zoomEvent
|
|
456
|
-
*/
|
|
457
|
-
_handleZoom(coords, zoomEvent) {
|
|
458
|
-
for (const [channel, resolutionSet] of Object.entries(
|
|
459
|
-
this._getZoomableResolutions()
|
|
460
|
-
)) {
|
|
461
|
-
if (resolutionSet.size <= 0) {
|
|
462
|
-
continue;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
const extents = this._getAxisExtents();
|
|
466
|
-
const childCoords = coords.shrink(
|
|
467
|
-
extents.add(this._getAxisOffsets())
|
|
468
|
-
);
|
|
469
|
-
|
|
470
|
-
const p = childCoords.normalizePoint(zoomEvent.x, zoomEvent.y);
|
|
471
|
-
const tp = childCoords.normalizePoint(
|
|
472
|
-
zoomEvent.x + zoomEvent.xDelta,
|
|
473
|
-
zoomEvent.y + zoomEvent.yDelta
|
|
474
|
-
);
|
|
475
|
-
|
|
476
|
-
const delta = {
|
|
477
|
-
x: tp.x - p.x,
|
|
478
|
-
y: tp.y - p.y,
|
|
479
|
-
};
|
|
480
|
-
|
|
481
|
-
for (const resolution of resolutionSet) {
|
|
482
|
-
resolution.zoom(
|
|
483
|
-
2 ** zoomEvent.zDelta,
|
|
484
|
-
channel == "y"
|
|
485
|
-
? 1 - p[/** @type {PositionalChannel} */ (channel)]
|
|
486
|
-
: p[/** @type {PositionalChannel} */ (channel)],
|
|
487
|
-
channel == "x" ? delta.x : -delta.y
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
this.context.animator.requestRender();
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
/**
|
|
497
|
-
* @param {import("../spec/view").ViewConfig} viewConfig
|
|
498
|
-
* @returns {import("../spec/view").UnitSpec}
|
|
499
|
-
*/
|
|
500
|
-
function createBackground(viewConfig) {
|
|
501
|
-
return {
|
|
502
|
-
configurableVisibility: false,
|
|
503
|
-
data: { values: [{}] },
|
|
504
|
-
mark: {
|
|
505
|
-
fill: null,
|
|
506
|
-
strokeWidth: 1.0,
|
|
507
|
-
...viewConfig,
|
|
508
|
-
type: "rect",
|
|
509
|
-
clip: false, // Shouldn't be needed
|
|
510
|
-
tooltip: null,
|
|
511
|
-
},
|
|
512
|
-
};
|
|
513
|
-
}
|