@genome-spy/core 0.58.1 → 0.60.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/bundle/{index-DwLfOHEk.js → index-5ajWdKly.js} +1 -1
- package/dist/bundle/{index-vgGDWUPz.js → index-B03-Om4z.js} +1 -1
- package/dist/bundle/index-Bg7C4Xat.js +2750 -0
- package/dist/bundle/{index-CalimFw3.js → index-C3QR8Lv6.js} +79 -79
- package/dist/bundle/{index-DKe9Bhvi.js → index-g8iXgW0W.js} +1 -1
- package/dist/bundle/index.es.js +6554 -6011
- package/dist/bundle/index.js +189 -164
- package/dist/bundle/{long-BviWyoZx.js → long-B-FASCSo.js} +45 -45
- package/dist/schema.json +312 -25
- package/dist/src/data/collector.d.ts.map +1 -1
- package/dist/src/data/collector.js +1 -0
- package/dist/src/data/flowNode.d.ts.map +1 -1
- package/dist/src/data/sources/dataSource.d.ts.map +1 -1
- package/dist/src/data/sources/dataUtils.d.ts +2 -1
- package/dist/src/data/sources/dataUtils.d.ts.map +1 -1
- package/dist/src/data/sources/dataUtils.js +3 -4
- package/dist/src/data/sources/inlineSource.d.ts +8 -0
- package/dist/src/data/sources/inlineSource.d.ts.map +1 -1
- package/dist/src/data/sources/inlineSource.js +17 -1
- package/dist/src/data/sources/urlSource.d.ts +1 -0
- package/dist/src/data/sources/urlSource.d.ts.map +1 -1
- package/dist/src/data/sources/urlSource.js +33 -4
- package/dist/src/data/transforms/identifier.d.ts.map +1 -1
- package/dist/src/data/transforms/measureText.js +1 -1
- package/dist/src/data/transforms/regexFold.d.ts.map +1 -1
- package/dist/src/data/transforms/regexFold.js +10 -0
- package/dist/src/data/transforms/regexFold.test.js +13 -0
- package/dist/src/encoder/encoder.d.ts +1 -1
- package/dist/src/fonts/bmFontManager.js +2 -2
- package/dist/src/fonts/bmFontMetrics.d.ts.map +1 -1
- package/dist/src/genomeSpy.d.ts.map +1 -1
- package/dist/src/genomeSpy.js +39 -19
- package/dist/src/gl/arrayBuilder.d.ts.map +1 -1
- package/dist/src/gl/colorUtils.d.ts +4 -0
- package/dist/src/gl/colorUtils.d.ts.map +1 -1
- package/dist/src/gl/colorUtils.js +8 -0
- package/dist/src/gl/glslScaleGenerator.d.ts +1 -1
- package/dist/src/gl/glslScaleGenerator.d.ts.map +1 -1
- package/dist/src/gl/glslScaleGenerator.js +1 -9
- package/dist/src/gl/includes/common.glsl.js +1 -1
- package/dist/src/gl/webGLHelper.d.ts +1 -1
- package/dist/src/gl/webGLHelper.d.ts.map +1 -1
- package/dist/src/marks/link.d.ts.map +1 -1
- package/dist/src/marks/link.js +9 -1
- package/dist/src/marks/mark.d.ts +8 -0
- package/dist/src/marks/mark.d.ts.map +1 -1
- package/dist/src/marks/mark.js +101 -3
- package/dist/src/marks/point.d.ts +1 -1
- package/dist/src/marks/point.d.ts.map +1 -1
- package/dist/src/marks/point.fragment.glsl.js +1 -1
- package/dist/src/marks/point.vertex.glsl.js +1 -1
- package/dist/src/marks/rect.common.glsl.js +1 -1
- package/dist/src/marks/rect.d.ts.map +1 -1
- package/dist/src/marks/rect.fragment.glsl.js +1 -1
- package/dist/src/marks/rect.js +41 -0
- package/dist/src/marks/rect.vertex.glsl.js +1 -1
- package/dist/src/selection/selection.d.ts +27 -2
- package/dist/src/selection/selection.d.ts.map +1 -1
- package/dist/src/selection/selection.js +53 -3
- package/dist/src/spec/data.d.ts +18 -1
- package/dist/src/spec/mark.d.ts +58 -1
- package/dist/src/spec/parameter.d.ts +71 -31
- package/dist/src/spec/sampleView.d.ts +12 -1
- package/dist/src/spec/view.d.ts +9 -2
- package/dist/src/styles/genome-spy.css.d.ts +1 -1
- package/dist/src/styles/genome-spy.css.d.ts.map +1 -1
- package/dist/src/styles/genome-spy.css.js +12 -1
- package/dist/src/styles/genome-spy.scss +19 -1
- package/dist/src/types/selectionTypes.d.ts +4 -7
- package/dist/src/types/viewContext.d.ts +0 -15
- package/dist/src/utils/expression.d.ts.map +1 -1
- package/dist/src/utils/expression.js +4 -0
- package/dist/src/utils/indexer.d.ts +0 -2
- package/dist/src/utils/indexer.d.ts.map +1 -1
- package/dist/src/utils/reservationMap.d.ts +4 -4
- package/dist/src/utils/reservationMap.d.ts.map +1 -1
- package/dist/src/utils/scaleNull.d.ts +0 -2
- package/dist/src/utils/scaleNull.d.ts.map +1 -1
- package/dist/src/utils/trees.d.ts +2 -2
- package/dist/src/utils/ui/tooltip.d.ts +6 -10
- package/dist/src/utils/ui/tooltip.d.ts.map +1 -1
- package/dist/src/utils/ui/tooltip.js +74 -42
- package/dist/src/view/concatView.d.ts +1 -1
- package/dist/src/view/concatView.d.ts.map +1 -1
- package/dist/src/view/concatView.js +1 -1
- package/dist/src/view/gridView/gridChild.d.ts +53 -0
- package/dist/src/view/gridView/gridChild.d.ts.map +1 -0
- package/dist/src/view/gridView/gridChild.js +753 -0
- package/dist/src/view/gridView/gridView.d.ts +64 -0
- package/dist/src/view/gridView/gridView.d.ts.map +1 -0
- package/dist/src/view/{gridView.js → gridView/gridView.js} +40 -595
- package/dist/src/view/gridView/scrollbar.d.ts +32 -0
- package/dist/src/view/gridView/scrollbar.d.ts.map +1 -0
- package/dist/src/view/gridView/scrollbar.js +186 -0
- package/dist/src/view/gridView/selectionRect.d.ts +10 -0
- package/dist/src/view/gridView/selectionRect.d.ts.map +1 -0
- package/dist/src/view/gridView/selectionRect.js +182 -0
- package/dist/src/view/layout/rectangle.d.ts +11 -1
- package/dist/src/view/layout/rectangle.d.ts.map +1 -1
- package/dist/src/view/layout/rectangle.js +22 -2
- package/dist/src/view/layout/rectangle.test.js +12 -0
- package/dist/src/view/paramMediator.d.ts.map +1 -1
- package/dist/src/view/paramMediator.js +11 -2
- package/dist/src/view/scaleResolution.d.ts +1 -0
- package/dist/src/view/scaleResolution.d.ts.map +1 -1
- package/dist/src/view/scaleResolution.js +43 -33
- package/dist/src/view/testUtils.d.ts.map +1 -1
- package/dist/src/view/testUtils.js +0 -4
- package/dist/src/view/view.d.ts +6 -0
- package/dist/src/view/view.d.ts.map +1 -1
- package/dist/src/view/view.js +19 -0
- package/dist/src/view/viewFactory.d.ts.map +1 -1
- package/dist/src/view/viewFactory.js +13 -1
- package/package.json +2 -2
- package/dist/bundle/index-DS2hvLgl.js +0 -3425
- package/dist/src/view/gridView.d.ts +0 -135
- package/dist/src/view/gridView.d.ts.map +0 -1
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
import { isContinuous } from "vega-scale";
|
|
2
|
+
import {
|
|
3
|
+
asSelectionConfig,
|
|
4
|
+
createIntervalSelection,
|
|
5
|
+
isActiveIntervalSelection,
|
|
6
|
+
isIntervalSelectionConfig,
|
|
7
|
+
selectionContainsPoint,
|
|
8
|
+
} from "../../selection/selection.js";
|
|
9
|
+
import AxisGridView from "../axisGridView.js";
|
|
10
|
+
import AxisView, { CHANNEL_ORIENTS } from "../axisView.js";
|
|
11
|
+
import LayerView from "../layerView.js";
|
|
12
|
+
import Padding from "../layout/padding.js";
|
|
13
|
+
import Point from "../layout/point.js";
|
|
14
|
+
import Rectangle from "../layout/rectangle.js";
|
|
15
|
+
import createTitle from "../title.js";
|
|
16
|
+
import UnitView from "../unitView.js";
|
|
17
|
+
import Scrollbar from "./scrollbar.js";
|
|
18
|
+
import SelectionRect from "./selectionRect.js";
|
|
19
|
+
|
|
20
|
+
export default class GridChild {
|
|
21
|
+
/**
|
|
22
|
+
* @param {import("../view.js").default} view
|
|
23
|
+
* @param {import("../containerView.js").default} layoutParent
|
|
24
|
+
* @param {number} serial
|
|
25
|
+
*/
|
|
26
|
+
constructor(view, layoutParent, serial) {
|
|
27
|
+
this.layoutParent = layoutParent;
|
|
28
|
+
this.view = view;
|
|
29
|
+
this.serial = serial;
|
|
30
|
+
|
|
31
|
+
/** @type {UnitView} */
|
|
32
|
+
this.background = undefined;
|
|
33
|
+
|
|
34
|
+
/** @type {UnitView} */
|
|
35
|
+
this.backgroundStroke = undefined;
|
|
36
|
+
|
|
37
|
+
/** @type {Partial<Record<import("../../spec/axis.js").AxisOrient, AxisView>>} axes */
|
|
38
|
+
this.axes = {};
|
|
39
|
+
|
|
40
|
+
/** @type {Partial<Record<import("../../spec/axis.js").AxisOrient, AxisGridView>>} gridLines */
|
|
41
|
+
this.gridLines = {};
|
|
42
|
+
|
|
43
|
+
/** @type {Partial<Record<import("./scrollbar.js").ScrollDirection, Scrollbar>>} */
|
|
44
|
+
this.scrollbars = {};
|
|
45
|
+
|
|
46
|
+
/** @type {SelectionRect} */
|
|
47
|
+
this.selectionRect = undefined;
|
|
48
|
+
|
|
49
|
+
/** @type {UnitView} */
|
|
50
|
+
this.title = undefined;
|
|
51
|
+
|
|
52
|
+
/** @type {Rectangle} */
|
|
53
|
+
this.coords = Rectangle.ZERO;
|
|
54
|
+
|
|
55
|
+
if (view.needsAxes.x || view.needsAxes.y) {
|
|
56
|
+
const spec = view.spec;
|
|
57
|
+
const viewBackground = "view" in spec ? spec?.view : undefined;
|
|
58
|
+
|
|
59
|
+
const backgroundSpec = createBackground(viewBackground);
|
|
60
|
+
if (backgroundSpec) {
|
|
61
|
+
this.background = new UnitView(
|
|
62
|
+
backgroundSpec,
|
|
63
|
+
layoutParent.context,
|
|
64
|
+
layoutParent,
|
|
65
|
+
view,
|
|
66
|
+
"background" + serial,
|
|
67
|
+
{
|
|
68
|
+
blockEncodingInheritance: true,
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const backgroundStrokeSpec = createBackgroundStroke(viewBackground);
|
|
74
|
+
if (backgroundStrokeSpec) {
|
|
75
|
+
this.backgroundStroke = new UnitView(
|
|
76
|
+
backgroundStrokeSpec,
|
|
77
|
+
layoutParent.context,
|
|
78
|
+
layoutParent,
|
|
79
|
+
view,
|
|
80
|
+
"backgroundStroke" + serial,
|
|
81
|
+
{
|
|
82
|
+
blockEncodingInheritance: true,
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const title = createTitle(view.spec.title);
|
|
88
|
+
if (title) {
|
|
89
|
+
const unitView = new UnitView(
|
|
90
|
+
title,
|
|
91
|
+
layoutParent.context,
|
|
92
|
+
layoutParent,
|
|
93
|
+
view,
|
|
94
|
+
"title" + serial,
|
|
95
|
+
{
|
|
96
|
+
blockEncodingInheritance: true,
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
this.title = unitView;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// TODO: More specific getter for this
|
|
104
|
+
if (view.spec.viewportWidth != null) {
|
|
105
|
+
this.scrollbars.horizontal = new Scrollbar(this, "horizontal");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (view.spec.viewportHeight != null) {
|
|
109
|
+
this.scrollbars.vertical = new Scrollbar(this, "vertical");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.#setupIntervalSelection();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#setupIntervalSelection() {
|
|
116
|
+
const view = this.view;
|
|
117
|
+
|
|
118
|
+
// TODO: Move to context
|
|
119
|
+
const setCursor = (/** @type {string} */ cursor) => {
|
|
120
|
+
this.view.context.glHelper.canvas.style.cursor = cursor;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// TODO: If the child is a LayerView, selection params should be pulled from its children as well
|
|
124
|
+
for (const [name, param] of view.paramMediator.paramConfigs) {
|
|
125
|
+
if (!("select" in param)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const select = asSelectionConfig(param.select);
|
|
130
|
+
|
|
131
|
+
if (!isIntervalSelectionConfig(select)) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const channels = select.encodings;
|
|
136
|
+
|
|
137
|
+
const scaleResolutions = Object.fromEntries(
|
|
138
|
+
channels.map((channel) => {
|
|
139
|
+
const resolution = this.view.getScaleResolution(channel);
|
|
140
|
+
const scale = resolution?.scale;
|
|
141
|
+
|
|
142
|
+
if (!scale || !isContinuous(scale.type)) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`No continuous scale found for interval selection param "${name}" on channel "${channel}"! Scale type is "${scale?.type ?? "none"}".`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return [channel, resolution];
|
|
148
|
+
})
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
if (this.selectionRect) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
"Only one interval selection per container is currently allowed!"
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// --- Validation and early exits done ---
|
|
158
|
+
|
|
159
|
+
let mouseOver = false;
|
|
160
|
+
let preventNextClickPropagation = false;
|
|
161
|
+
let nowBrushing = false;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Selection rectangle in screen coordinates. Used when translating
|
|
165
|
+
* an existing selection.
|
|
166
|
+
* @type {Rectangle}
|
|
167
|
+
*/
|
|
168
|
+
let translatedRectangle = null;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @param {{x: number, y: number}} a
|
|
172
|
+
* @param {{x: number, y: number}} b
|
|
173
|
+
* @return {Partial<Record<import("../../spec/channel.js").PrimaryPositionalChannel, [number, number]>>}
|
|
174
|
+
*/
|
|
175
|
+
const pointsToIntervals = (a, b) =>
|
|
176
|
+
Object.fromEntries(
|
|
177
|
+
channels.map((channel) => [
|
|
178
|
+
channel,
|
|
179
|
+
[
|
|
180
|
+
Math.min(a[channel], b[channel]),
|
|
181
|
+
Math.max(a[channel], b[channel]),
|
|
182
|
+
],
|
|
183
|
+
])
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const selectionExpr = view.paramMediator.createExpression(name);
|
|
187
|
+
const setter = view.paramMediator.getSetter(name);
|
|
188
|
+
|
|
189
|
+
if (param.value) {
|
|
190
|
+
setter({ type: "interval", intervals: param.value });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const clearSelection = () => {
|
|
194
|
+
setter(createIntervalSelection(channels));
|
|
195
|
+
setCursor(null);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
this.selectionRect = new SelectionRect(
|
|
199
|
+
this,
|
|
200
|
+
selectionExpr,
|
|
201
|
+
select.mark
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// WARNING! The following is an async method! Seems to work (by chance).
|
|
205
|
+
// TODO: Should be called and awaited in a sensible place. Maybe provide some
|
|
206
|
+
// registration logic for such post-creation initializations?
|
|
207
|
+
this.selectionRect.initializeChildren();
|
|
208
|
+
|
|
209
|
+
const invertPoint = (
|
|
210
|
+
/** @type {import("../layout/point.js").default} */ point
|
|
211
|
+
) => {
|
|
212
|
+
const inverted = { x: 0, y: 0 };
|
|
213
|
+
|
|
214
|
+
const np = view.coords.normalizePoint(point.x, point.y, true);
|
|
215
|
+
|
|
216
|
+
for (const channel of channels) {
|
|
217
|
+
const scale = scaleResolutions[channel].scale;
|
|
218
|
+
// @ts-ignore
|
|
219
|
+
const val = scale.invert(channel == "x" ? np.x : np.y);
|
|
220
|
+
inverted[channel] =
|
|
221
|
+
val +
|
|
222
|
+
(["index", "locus"].includes(scale.type) ? 0.5 : 0);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return inverted;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Converts the current selection intervals (in scale domain) to a rectangle
|
|
230
|
+
* in screen coordinates.
|
|
231
|
+
* @param {import("../../types/selectionTypes.js").IntervalSelection} selection
|
|
232
|
+
*/
|
|
233
|
+
const selectionToRect = (selection) => {
|
|
234
|
+
const { intervals } = selection;
|
|
235
|
+
|
|
236
|
+
const mapCorner = (
|
|
237
|
+
/** @type {number} */ xVal,
|
|
238
|
+
/** @type {number} */ yVal,
|
|
239
|
+
/** @type {number} */ i
|
|
240
|
+
) => {
|
|
241
|
+
const getCoord = (
|
|
242
|
+
/** @type {import("../../spec/channel.js").PrimaryPositionalChannel} */ channel,
|
|
243
|
+
/** @type {number} */ val
|
|
244
|
+
) => {
|
|
245
|
+
if (val == null) return null;
|
|
246
|
+
return scaleResolutions[channel].scale(val);
|
|
247
|
+
};
|
|
248
|
+
const px = getCoord("x", xVal) ?? i;
|
|
249
|
+
const py = getCoord("y", yVal) ?? i;
|
|
250
|
+
return view.coords.denormalizePoint(px, py, true);
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const a = mapCorner(intervals.x?.[0], intervals.y?.[0], 0);
|
|
254
|
+
const b = mapCorner(intervals.x?.[1], intervals.y?.[1], 1);
|
|
255
|
+
|
|
256
|
+
return Rectangle.create(a.x, a.y, b.x - a.x, b.y - a.y);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
view.addInteractionEventListener("mousedown", (coords, event) => {
|
|
260
|
+
if (/** @type {MouseEvent} */ (event.uiEvent).button != 0) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Coordinates of the selection rectangle, if it exists.
|
|
265
|
+
// Must be operated in the view's coordinate system, not in data domain,
|
|
266
|
+
// as non-linear scales may be used.
|
|
267
|
+
translatedRectangle = mouseOver
|
|
268
|
+
? selectionToRect(selectionExpr())
|
|
269
|
+
: null;
|
|
270
|
+
|
|
271
|
+
if (translatedRectangle) {
|
|
272
|
+
// Started dragging an existing selection
|
|
273
|
+
setCursor("grabbing");
|
|
274
|
+
// Start of dragging should prevent click propagation so that
|
|
275
|
+
// no other selections or events are triggered.
|
|
276
|
+
preventNextClickPropagation = true;
|
|
277
|
+
} else {
|
|
278
|
+
const mouseDownPoint = event.point;
|
|
279
|
+
if (isActiveIntervalSelection(selectionExpr())) {
|
|
280
|
+
// If there's a selection, prevent the next click from propagating.
|
|
281
|
+
// The first click will clear the selection, not trigger
|
|
282
|
+
// any other possible selections.
|
|
283
|
+
preventNextClickPropagation = true;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (/** @type {MouseEvent} */ (event.uiEvent).shiftKey) {
|
|
287
|
+
// Start brushing a new selection, clear the existing selection
|
|
288
|
+
clearSelection();
|
|
289
|
+
nowBrushing = true;
|
|
290
|
+
} else if (isActiveIntervalSelection(selectionExpr())) {
|
|
291
|
+
// If mouse button is released and there was a selection,
|
|
292
|
+
// it should be cleared unless the viewport was panned by dragging.
|
|
293
|
+
/** @type {import("../view.js").InteractionEventListener} */
|
|
294
|
+
const listener = (coords, event) => {
|
|
295
|
+
view.removeInteractionEventListener(
|
|
296
|
+
"mouseup",
|
|
297
|
+
listener
|
|
298
|
+
);
|
|
299
|
+
const mouseUpPoint = event.point;
|
|
300
|
+
|
|
301
|
+
// Retain selection if the viewport is panned by dragging
|
|
302
|
+
const movementThreshold = 2; // pixels
|
|
303
|
+
if (
|
|
304
|
+
mouseDownPoint.subtract(mouseUpPoint).length <
|
|
305
|
+
movementThreshold
|
|
306
|
+
) {
|
|
307
|
+
clearSelection();
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
view.addInteractionEventListener("mouseup", listener);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Prevent panning interaction
|
|
316
|
+
event.stopPropagation();
|
|
317
|
+
|
|
318
|
+
const start = event.point;
|
|
319
|
+
const viewOffset = Point.fromMouseEvent(
|
|
320
|
+
/** @type {MouseEvent} */ (event.uiEvent)
|
|
321
|
+
).subtract(start);
|
|
322
|
+
|
|
323
|
+
const mouseMoveListener = (/** @type {MouseEvent} */ event) => {
|
|
324
|
+
// This listener is added to the document so that events are captured even if the mouse leaves the view.
|
|
325
|
+
// Thus, coordinates need to be adjusted to the view's coordinate system.
|
|
326
|
+
const current =
|
|
327
|
+
Point.fromMouseEvent(event).subtract(viewOffset);
|
|
328
|
+
|
|
329
|
+
/** @type {ReturnType<typeof pointsToIntervals>} */
|
|
330
|
+
let intervals;
|
|
331
|
+
|
|
332
|
+
if (translatedRectangle) {
|
|
333
|
+
const offset = current.subtract(start);
|
|
334
|
+
const newRect = translatedRectangle.translate(
|
|
335
|
+
offset.x,
|
|
336
|
+
offset.y
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
intervals = pointsToIntervals(
|
|
340
|
+
invertPoint(new Point(newRect.x, newRect.y)),
|
|
341
|
+
invertPoint(new Point(newRect.x2, newRect.y2))
|
|
342
|
+
);
|
|
343
|
+
} else {
|
|
344
|
+
intervals = pointsToIntervals(
|
|
345
|
+
invertPoint(start),
|
|
346
|
+
invertPoint(current)
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
for (const channel of channels) {
|
|
351
|
+
const scaleResolution = scaleResolutions[channel];
|
|
352
|
+
const { zoomExtent, scale } = scaleResolution;
|
|
353
|
+
const interval = intervals[channel];
|
|
354
|
+
|
|
355
|
+
if (["index", "locus"].includes(scale.type)) {
|
|
356
|
+
// These scales use integer values. Need to round them.
|
|
357
|
+
interval[0] = Math.ceil(interval[0]);
|
|
358
|
+
interval[1] = Math.ceil(interval[1]);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (translatedRectangle) {
|
|
362
|
+
// When dragging, clamp the interval so that the size stays the same and the interval doesn't exceed zoomExtent
|
|
363
|
+
const size = interval[1] - interval[0];
|
|
364
|
+
const min = zoomExtent[0];
|
|
365
|
+
const max = zoomExtent[1];
|
|
366
|
+
|
|
367
|
+
// Clamp the start and end so the interval stays within bounds
|
|
368
|
+
// Note: Only works reliably with linear scales. TODO: Handle other scales.
|
|
369
|
+
if (interval[0] < min) {
|
|
370
|
+
interval[0] = min;
|
|
371
|
+
interval[1] = min + size;
|
|
372
|
+
}
|
|
373
|
+
if (interval[1] > max) {
|
|
374
|
+
interval[1] = max;
|
|
375
|
+
interval[0] = max - size;
|
|
376
|
+
}
|
|
377
|
+
} else {
|
|
378
|
+
interval[0] = Math.max(zoomExtent[0], interval[0]);
|
|
379
|
+
interval[1] = Math.min(zoomExtent[1], interval[1]);
|
|
380
|
+
}
|
|
381
|
+
interval[1] = Math.min(zoomExtent[1], interval[1]);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
setter({ type: "interval", intervals });
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const mouseUpListener = () => {
|
|
388
|
+
document.removeEventListener(
|
|
389
|
+
"mousemove",
|
|
390
|
+
mouseMoveListener
|
|
391
|
+
);
|
|
392
|
+
document.removeEventListener("mouseup", mouseUpListener);
|
|
393
|
+
|
|
394
|
+
nowBrushing = false;
|
|
395
|
+
if (translatedRectangle) {
|
|
396
|
+
setCursor("move");
|
|
397
|
+
translatedRectangle = null;
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
document.addEventListener("mousemove", mouseMoveListener);
|
|
401
|
+
|
|
402
|
+
document.addEventListener("mouseup", mouseUpListener);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
view.addInteractionEventListener(
|
|
406
|
+
"click",
|
|
407
|
+
(coords, event) => {
|
|
408
|
+
if (/** @type {MouseEvent} */ (event.uiEvent).button == 0) {
|
|
409
|
+
if (preventNextClickPropagation) {
|
|
410
|
+
event.stopPropagation();
|
|
411
|
+
preventNextClickPropagation = false;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
true
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
const isPointInsideSelection = (/** @type {Point} */ point) =>
|
|
419
|
+
selectionContainsPoint(selectionExpr(), invertPoint(point));
|
|
420
|
+
|
|
421
|
+
// TODO: Make behavior configurable
|
|
422
|
+
view.addInteractionEventListener(
|
|
423
|
+
"dblclick",
|
|
424
|
+
(coords, event) => {
|
|
425
|
+
if (isPointInsideSelection(event.point)) {
|
|
426
|
+
clearSelection();
|
|
427
|
+
event.stopPropagation();
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
true
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
// Handle mouse cursor changes
|
|
434
|
+
view.addInteractionEventListener("mousemove", (coords, event) => {
|
|
435
|
+
if (isPointInsideSelection(event.point)) {
|
|
436
|
+
// Brushing and translating the existing brush are different actions.
|
|
437
|
+
if (!nowBrushing) {
|
|
438
|
+
mouseOver = true;
|
|
439
|
+
// When translation is active, the cursor shows a grabbing hand.
|
|
440
|
+
if (!translatedRectangle) {
|
|
441
|
+
setCursor("move");
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
mouseOver = false;
|
|
446
|
+
if (!translatedRectangle) {
|
|
447
|
+
setCursor(null);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
*getChildren() {
|
|
455
|
+
if (this.background) {
|
|
456
|
+
yield this.background;
|
|
457
|
+
}
|
|
458
|
+
if (this.backgroundStroke) {
|
|
459
|
+
yield this.backgroundStroke;
|
|
460
|
+
}
|
|
461
|
+
if (this.title) {
|
|
462
|
+
yield this.title;
|
|
463
|
+
}
|
|
464
|
+
yield* Object.values(this.axes);
|
|
465
|
+
yield* Object.values(this.gridLines);
|
|
466
|
+
yield this.view;
|
|
467
|
+
yield* Object.values(this.scrollbars);
|
|
468
|
+
if (this.selectionRect) {
|
|
469
|
+
yield this.selectionRect;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Create view decorations, grid lines, axes, etc.
|
|
475
|
+
*/
|
|
476
|
+
async createAxes() {
|
|
477
|
+
const { view, axes, gridLines } = this;
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* @param {import("../axisResolution.js").default} r
|
|
481
|
+
* @param {import("../../spec/channel.js").PrimaryPositionalChannel} channel
|
|
482
|
+
*/
|
|
483
|
+
const getAxisPropsWithDefaults = (r, channel) => {
|
|
484
|
+
const propsWithoutDefaults = r.getAxisProps();
|
|
485
|
+
if (propsWithoutDefaults === null) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const props = propsWithoutDefaults
|
|
490
|
+
? { ...propsWithoutDefaults }
|
|
491
|
+
: {};
|
|
492
|
+
|
|
493
|
+
// Pick a default orient based on what is available.
|
|
494
|
+
// This logic is needed for layer views that have independent axes.
|
|
495
|
+
if (!props.orient) {
|
|
496
|
+
for (const orient of CHANNEL_ORIENTS[channel]) {
|
|
497
|
+
if (!axes[orient]) {
|
|
498
|
+
props.orient = orient;
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (!props.orient) {
|
|
503
|
+
throw new Error(
|
|
504
|
+
"No slots available for an axis! Perhaps a LayerView has more than two children?"
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
props.title ??= r.getTitle();
|
|
510
|
+
|
|
511
|
+
if (!CHANNEL_ORIENTS[channel].includes(props.orient)) {
|
|
512
|
+
throw new Error(
|
|
513
|
+
`Invalid axis orientation "${props.orient}" on channel "${channel}"!`
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return props;
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* @param {import("../axisResolution.js").default} r
|
|
522
|
+
* @param {import("../../spec/channel.js").PrimaryPositionalChannel} channel
|
|
523
|
+
* @param {import("../view.js").default} axisParent
|
|
524
|
+
*/
|
|
525
|
+
const createAxis = async (r, channel, axisParent) => {
|
|
526
|
+
const props = getAxisPropsWithDefaults(r, channel);
|
|
527
|
+
|
|
528
|
+
if (props) {
|
|
529
|
+
if (axes[props.orient]) {
|
|
530
|
+
throw new Error(
|
|
531
|
+
`An axis with the orient "${props.orient}" already exists!`
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const axisView = new AxisView(
|
|
536
|
+
props,
|
|
537
|
+
r.scaleResolution.type,
|
|
538
|
+
this.layoutParent.context,
|
|
539
|
+
this.layoutParent,
|
|
540
|
+
axisParent
|
|
541
|
+
);
|
|
542
|
+
axes[props.orient] = axisView;
|
|
543
|
+
await axisView.initializeChildren();
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* @param {import("../axisResolution.js").default} r
|
|
549
|
+
* @param {import("../../spec/channel.js").PrimaryPositionalChannel} channel
|
|
550
|
+
* @param {import("../view.js").default} axisParent
|
|
551
|
+
*/
|
|
552
|
+
const createAxisGrid = async (r, channel, axisParent) => {
|
|
553
|
+
const props = getAxisPropsWithDefaults(r, channel);
|
|
554
|
+
|
|
555
|
+
if (props && (props.grid || props.chromGrid)) {
|
|
556
|
+
const axisGridView = new AxisGridView(
|
|
557
|
+
props,
|
|
558
|
+
r.scaleResolution.type,
|
|
559
|
+
this.layoutParent.context,
|
|
560
|
+
this.layoutParent,
|
|
561
|
+
axisParent
|
|
562
|
+
);
|
|
563
|
+
gridLines[props.orient] = axisGridView;
|
|
564
|
+
await axisGridView.initializeChildren();
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
// Handle children that have caught axis resolutions. Create axes for them.
|
|
569
|
+
for (const channel of /** @type {import("../../spec/channel.js").PrimaryPositionalChannel[]} */ ([
|
|
570
|
+
"x",
|
|
571
|
+
"y",
|
|
572
|
+
])) {
|
|
573
|
+
if (view.needsAxes[channel]) {
|
|
574
|
+
const r = view.resolutions.axis[channel];
|
|
575
|
+
if (!r) {
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
await createAxis(r, channel, view);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Handle gridlines of children. Note: children's axis resolution may be caught by
|
|
584
|
+
// this view or some of this view's ancestors.
|
|
585
|
+
for (const channel of /** @type {import("../../spec/channel.js").PrimaryPositionalChannel[]} */ ([
|
|
586
|
+
"x",
|
|
587
|
+
"y",
|
|
588
|
+
])) {
|
|
589
|
+
if (
|
|
590
|
+
view.needsAxes[channel] &&
|
|
591
|
+
// Handle a special case where the child view has an excluded resolution
|
|
592
|
+
// but no scale or axis, e.g., when only values are used on a channel.
|
|
593
|
+
view.getConfiguredOrDefaultResolution(channel, "axis") !=
|
|
594
|
+
"excluded"
|
|
595
|
+
) {
|
|
596
|
+
const r = view.getAxisResolution(channel);
|
|
597
|
+
if (!r) {
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
await createAxisGrid(r, channel, view);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Handle LayerView's possible independent axes
|
|
606
|
+
if (view instanceof LayerView) {
|
|
607
|
+
// First create axes that have an orient preference
|
|
608
|
+
for (const layerChild of view) {
|
|
609
|
+
for (const [channel, r] of Object.entries(
|
|
610
|
+
layerChild.resolutions.axis
|
|
611
|
+
)) {
|
|
612
|
+
const props = r.getAxisProps();
|
|
613
|
+
if (props && props.orient) {
|
|
614
|
+
await createAxis(r, channel, layerChild);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Then create axes in a priority order
|
|
620
|
+
for (const layerChild of view) {
|
|
621
|
+
for (const [channel, r] of Object.entries(
|
|
622
|
+
layerChild.resolutions.axis
|
|
623
|
+
)) {
|
|
624
|
+
const props = r.getAxisProps();
|
|
625
|
+
if (props && !props.orient) {
|
|
626
|
+
await createAxis(r, channel, layerChild);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// TODO: Axis grid
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Axes are created after scales are resolved, so we need to resolve possible new scales here
|
|
635
|
+
[...Object.values(axes), ...Object.values(gridLines)].forEach((v) =>
|
|
636
|
+
v.visit((view) => {
|
|
637
|
+
if (view instanceof UnitView) {
|
|
638
|
+
view.resolve("scale");
|
|
639
|
+
}
|
|
640
|
+
})
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
getOverhang() {
|
|
645
|
+
const calculate = (
|
|
646
|
+
/** @type {import("../../spec/axis.js").AxisOrient} */ orient
|
|
647
|
+
) => {
|
|
648
|
+
const axisView = this.axes[orient];
|
|
649
|
+
return axisView
|
|
650
|
+
? Math.max(
|
|
651
|
+
axisView.getPerpendicularSize() +
|
|
652
|
+
(axisView.axisProps.offset ?? 0),
|
|
653
|
+
0
|
|
654
|
+
)
|
|
655
|
+
: 0;
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
// Axes and overhang should be mutually exclusive, so we can just add them together
|
|
659
|
+
return new Padding(
|
|
660
|
+
calculate("top"),
|
|
661
|
+
calculate("right"),
|
|
662
|
+
calculate("bottom"),
|
|
663
|
+
calculate("left")
|
|
664
|
+
).add(this.view.getOverhang());
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
getOverhangAndPadding() {
|
|
668
|
+
return this.getOverhang().add(this.view.getPadding());
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* @param {import("../../spec/view.js").ViewBackground} viewBackground
|
|
674
|
+
* @returns {import("../../spec/view.js").UnitSpec}
|
|
675
|
+
*/
|
|
676
|
+
export function createBackground(viewBackground) {
|
|
677
|
+
const required =
|
|
678
|
+
viewBackground?.fill ||
|
|
679
|
+
viewBackground?.fillOpacity ||
|
|
680
|
+
viewBackground?.shadowOpacity;
|
|
681
|
+
if (!required) {
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
return {
|
|
686
|
+
configurableVisibility: false,
|
|
687
|
+
data: { values: [{}] },
|
|
688
|
+
mark: {
|
|
689
|
+
color: viewBackground.fill,
|
|
690
|
+
opacity:
|
|
691
|
+
viewBackground.fillOpacity ?? (viewBackground.fill ? 1.0 : 0.0),
|
|
692
|
+
type: "rect",
|
|
693
|
+
clip: false, // Shouldn't be needed
|
|
694
|
+
tooltip: null,
|
|
695
|
+
minHeight: 1,
|
|
696
|
+
minOpacity: 0,
|
|
697
|
+
shadowBlur: viewBackground.shadowBlur,
|
|
698
|
+
shadowColor: viewBackground.shadowColor,
|
|
699
|
+
shadowOffsetX: viewBackground.shadowOffsetX,
|
|
700
|
+
shadowOffsetY: viewBackground.shadowOffsetY,
|
|
701
|
+
shadowOpacity: viewBackground.shadowOpacity,
|
|
702
|
+
},
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* @param {import("../../spec/view.js").ViewBackground} viewBackground
|
|
708
|
+
* @returns {import("../../spec/view.js").UnitSpec}
|
|
709
|
+
*/
|
|
710
|
+
export function createBackgroundStroke(viewBackground) {
|
|
711
|
+
if (
|
|
712
|
+
!viewBackground ||
|
|
713
|
+
!viewBackground.stroke ||
|
|
714
|
+
viewBackground.strokeWidth === 0 ||
|
|
715
|
+
viewBackground.strokeOpacity === 0
|
|
716
|
+
) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Using rules to draw a non-filled rectangle.
|
|
721
|
+
// We are not using a rect mark because it is not optimized for outlines.
|
|
722
|
+
// TODO: Implement "hollow" mesh for non-filled rectangles
|
|
723
|
+
return {
|
|
724
|
+
configurableVisibility: false,
|
|
725
|
+
resolve: {
|
|
726
|
+
scale: { x: "excluded", y: "excluded" },
|
|
727
|
+
axis: { x: "excluded", y: "excluded" },
|
|
728
|
+
},
|
|
729
|
+
data: {
|
|
730
|
+
values: [
|
|
731
|
+
{ x: 0, y: 0, x2: 1, y2: 0 },
|
|
732
|
+
{ x: 1, y: 0, x2: 1, y2: 1 },
|
|
733
|
+
{ x: 1, y: 1, x2: 0, y2: 1 },
|
|
734
|
+
{ x: 0, y: 1, x2: 0, y2: 0 },
|
|
735
|
+
],
|
|
736
|
+
},
|
|
737
|
+
mark: {
|
|
738
|
+
size: viewBackground.strokeWidth ?? 1.0,
|
|
739
|
+
color: viewBackground.stroke ?? "lightgray",
|
|
740
|
+
strokeCap: "square",
|
|
741
|
+
opacity: viewBackground.strokeOpacity ?? 1.0,
|
|
742
|
+
type: "rule",
|
|
743
|
+
clip: false,
|
|
744
|
+
tooltip: null,
|
|
745
|
+
},
|
|
746
|
+
encoding: {
|
|
747
|
+
x: { field: "x", type: "quantitative", scale: null },
|
|
748
|
+
y: { field: "y", type: "quantitative", scale: null },
|
|
749
|
+
x2: { field: "x2" },
|
|
750
|
+
y2: { field: "y2" },
|
|
751
|
+
},
|
|
752
|
+
};
|
|
753
|
+
}
|