higlass 1.12.3 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +3 -3
  2. package/app/globals.d.ts +11 -0
  3. package/app/missing-types.d.ts +21 -0
  4. package/app/scripts/CenterTrack.jsx +0 -1
  5. package/app/scripts/DraggableDiv.jsx +0 -1
  6. package/app/scripts/GenomePositionSearchBox.jsx +0 -1
  7. package/app/scripts/SearchField.js +1 -1
  8. package/app/scripts/TrackRenderer.jsx +405 -89
  9. package/app/scripts/VerticalTiledPlot.jsx +0 -1
  10. package/app/scripts/configs/default-tracks-for-datatype.js +3 -2
  11. package/app/scripts/configs/primitives.js +3 -2
  12. package/app/scripts/configs/tracks-info-by-type.js +4 -1
  13. package/app/scripts/configs/tracks-info.js +25 -1
  14. package/app/scripts/plugins/available-for-plugins.js +14 -6
  15. package/app/scripts/types.ts +79 -0
  16. package/app/scripts/utils/abs-to-chr.js +16 -1
  17. package/app/scripts/utils/accessor-transposition.js +5 -4
  18. package/app/scripts/utils/add-arrays.js +9 -7
  19. package/app/scripts/utils/add-class.js +4 -2
  20. package/app/scripts/utils/add-event-listener-once.js +9 -3
  21. package/app/scripts/utils/background-task-scheduler.js +58 -2
  22. package/app/scripts/utils/base64-to-canvas.js +12 -5
  23. package/app/scripts/utils/chr-to-abs.js +10 -0
  24. package/app/scripts/utils/chrom-info-bisector.js +4 -1
  25. package/app/scripts/utils/clone-event.js +11 -4
  26. package/app/scripts/utils/color-to-hex.js +8 -0
  27. package/app/scripts/utils/color-to-rgba.js +8 -0
  28. package/app/scripts/utils/data-to-genomic-loci.js +13 -1
  29. package/app/scripts/utils/debounce.js +16 -11
  30. package/app/scripts/utils/dec-to-hex-str.js +7 -0
  31. package/app/scripts/utils/dict-from-tuples.js +11 -3
  32. package/app/scripts/utils/dict-items.js +15 -0
  33. package/app/scripts/utils/dict-keys.js +11 -1
  34. package/app/scripts/utils/dict-values.js +7 -0
  35. package/app/scripts/utils/download.js +14 -11
  36. package/app/scripts/utils/flatten.js +5 -2
  37. package/app/scripts/utils/for-each.js +7 -5
  38. package/app/scripts/utils/forward-event.js +3 -2
  39. package/app/scripts/utils/genome-loci-to-pixels.js +8 -0
  40. package/app/scripts/utils/genomic-range-to-chromosome-chunks.js +13 -6
  41. package/app/scripts/utils/get-aggregation-function.js +10 -2
  42. package/app/scripts/utils/get-element-dim.js +6 -0
  43. package/app/scripts/utils/gradient.js +14 -0
  44. package/app/scripts/utils/has-class.js +6 -4
  45. package/app/scripts/utils/hex-string-to-int.js +11 -4
  46. package/app/scripts/utils/index.js +14 -0
  47. package/app/scripts/utils/into-the-void.js +2 -1
  48. package/app/scripts/utils/is-track-or-child-track.js +6 -0
  49. package/app/scripts/utils/is-track-range-selectable.js +10 -1
  50. package/app/scripts/utils/is-within.js +9 -7
  51. package/app/scripts/utils/lat-to-y.js +6 -3
  52. package/app/scripts/utils/lng-to-x.js +4 -3
  53. package/app/scripts/utils/map.js +5 -2
  54. package/app/scripts/utils/max-non-zero.js +6 -0
  55. package/app/scripts/utils/max.js +4 -3
  56. package/app/scripts/utils/min-non-zero.js +6 -0
  57. package/app/scripts/utils/min.js +4 -3
  58. package/app/scripts/utils/mod.js +4 -3
  59. package/app/scripts/utils/numericify-version.js +5 -0
  60. package/app/scripts/utils/obj-vals.js +3 -2
  61. package/app/scripts/utils/or.js +4 -3
  62. package/app/scripts/utils/parse-chromsizes-rows.js +26 -5
  63. package/app/scripts/utils/q.js +3 -2
  64. package/app/scripts/utils/rad-to-deg.js +3 -2
  65. package/app/scripts/utils/reduce.js +2 -2
  66. package/app/scripts/utils/rel-to-abs-chrom-pos.js +10 -0
  67. package/app/scripts/utils/remove-class.js +3 -2
  68. package/app/scripts/utils/reset-d3-brush-style.js +7 -2
  69. package/app/scripts/utils/rgb-to-hex.js +9 -0
  70. package/app/scripts/utils/scales-center-and-k.js +5 -3
  71. package/app/scripts/utils/scales-to-genome-loci.js +10 -0
  72. package/app/scripts/utils/selected-items-to-cum-weights.js +12 -4
  73. package/app/scripts/utils/selected-items-to-size.js +5 -2
  74. package/app/scripts/utils/show-mouse-position.js +62 -19
  75. package/app/scripts/utils/some.js +6 -4
  76. package/app/scripts/utils/sum.js +4 -3
  77. package/app/scripts/utils/throttle-and-debounce.js +15 -6
  78. package/app/scripts/utils/tile-to-canvas.js +8 -4
  79. package/app/scripts/utils/timeout.js +2 -0
  80. package/app/scripts/utils/to-void.js +2 -0
  81. package/app/scripts/utils/total-track-pixel-height.js +11 -12
  82. package/app/scripts/utils/trim-trailing-slash.js +3 -2
  83. package/app/scripts/utils/type-guards.js +17 -0
  84. package/app/scripts/utils/value-to-color.js +7 -6
  85. package/app/scripts/utils/visit-positioned-tracks.js +16 -11
  86. package/app/scripts/utils/visit-tracks.js +12 -13
  87. package/dist/hglib.css +54 -54
  88. package/dist/hglib.js +62191 -62096
  89. package/dist/hglib.min.js +127 -127
  90. package/package.json +16 -5
@@ -1,3 +1,4 @@
1
+ // @ts-check
1
2
  import React from 'react';
2
3
  import PropTypes from 'prop-types';
3
4
 
@@ -72,6 +73,8 @@ import {
72
73
  trimTrailingSlash,
73
74
  } from './utils';
74
75
 
76
+ import { isCombinedTrackConfig, isWheelEvent } from './utils/type-guards';
77
+
75
78
  // Configs
76
79
  import { GLOBALS, THEME_DARK, TRACKS_INFO_BY_TYPE } from './configs';
77
80
 
@@ -85,6 +88,141 @@ const { getDataFetcher } = AVAILABLE_FOR_PLUGINS.dataFetchers;
85
88
 
86
89
  const SCROLL_TIMEOUT = 100;
87
90
 
91
+ /** @typedef {import('./types').Scale} Scale */
92
+ /** @typedef {import('./types').TrackConfig} TrackConfig */
93
+ /** @typedef {import('./types').TrackObject} TrackObject */
94
+
95
+ /** @typedef {TrackRenderer["setCenter"]} SetCentersFunction */
96
+ /** @typedef {(x: Scale, y: Scale) => [Scale, Scale]} ProjectorFunction */
97
+
98
+ /**
99
+ * @typedef TrackDefinition
100
+ * @property {TrackConfig} track
101
+ * @property {number} width
102
+ * @property {number} height
103
+ * @property {number} top
104
+ * @property {number} left
105
+ */
106
+
107
+ /** @typedef {Record<string, unknown>} TilesetInfo */
108
+
109
+ /**
110
+ * @typedef MetaPluginTrackContext
111
+ * @property {(trackId: string) => TrackObject | undefined} getTrackObject
112
+ * @property {() => void} onNewTilesLoaded
113
+ * @property {TrackConfig} definition
114
+ */
115
+
116
+ /**
117
+ * @typedef {Object} PluginTrackContext
118
+ * @property {string} id
119
+ * @property {string} trackUid
120
+ * @property {string} trackType
121
+ * @property {string} viewUid
122
+ * @property {import('pub-sub-es').PubSub} pubSub
123
+ * @property {import("pixi.js").Graphics} scene
124
+ * @property {Record<string, unknown>} dataConfig
125
+ * @property {unknown} dataFetcher
126
+ * @property {() => unknown} getLockGroupExtrema
127
+ * @property {(tilesetInfo: TilesetInfo) => void} handleTilesetInfoReceived
128
+ * @property {() => void} animate
129
+ * @property {HTMLElement} svgElement
130
+ * @property {() => boolean} isValueScaleLocked
131
+ * @property {() => void} onValueScaleChanged
132
+ * @property {(newOption: Record<string, unknown>) => void} onTrackOptionsChanged
133
+ * @property {() => void} onMouseMoveZoom
134
+ * @property {string} chromInfoPath
135
+ * @property {() => boolean} isShowGlobalMousePosition
136
+ * @property {() => (string | typeof THEME_DARK)} getTheme
137
+ * @property {unknown=} AVAILABLE_FOR_PLUGINS
138
+ * @property {(HTMLDivElement | null)=} baseEl
139
+ * @property {TrackConfig=} definition
140
+ * @property {number=} x
141
+ * @property {number=} y
142
+ * @property {number=} xPosition
143
+ * @property {number=} yPosition
144
+ * @property {[number, number]=} projectionXDomain
145
+ * @property {[number, number]=} projectionYDomain
146
+ * @property {unknown=} registerViewportChanged
147
+ * @property {unknown=} removeViewportChanged
148
+ * @property {unknown=} setDomainsCallback
149
+ * @property {TrackConfig[]=} tracks
150
+ * @property {TrackRenderer["createTrackObject"]=} createTrackObject
151
+ * @property {string=} orientation
152
+ * @property {boolean=} isOverlay
153
+ */
154
+
155
+ /**
156
+ * @typedef PluginTrack
157
+ * @property {{ new (availableForPlugins: unknown, context: PluginTrackContext, options: Record<string, unknown>): TrackObject }} track
158
+ * @property {false=} isMetaTrack
159
+ */
160
+
161
+ /**
162
+ * @typedef MetaPluginTrack
163
+ * @property {{ new (availableForPlugins: unknown, context: MetaPluginTrackContext, options: Record<string, unknown>): TrackObject }} track
164
+ * @property {true} isMetaTrack
165
+ */
166
+
167
+ /**
168
+ * @template T
169
+ * @typedef {T & { __zoom?: import('d3-zoom').ZoomTransform }} WithZoomTransform
170
+ */
171
+
172
+ /**
173
+ * @typedef TrackRendererProps
174
+ * @property {HTMLElement} canvasElement
175
+ * @property {number} centerHeight
176
+ * @property {number} centerWidth
177
+ * @property {Array<JSX.Element>} children
178
+ * @property {number} galleryDim
179
+ * @property {number} height
180
+ * @property {[number, number]} initialXDomain
181
+ * @property {[number, number]} initialYDomain
182
+ * @property {boolean} isShowGlobalMousePosition
183
+ * @property {boolean} isRangeSelection
184
+ * @property {number} leftWidth
185
+ * @property {number} leftWidthNoGallery
186
+ * @property {number} paddingLeft
187
+ * @property {number} paddingTop
188
+ * @property {Array<TrackConfig>} metaTracks
189
+ * @property {() => void} onMouseMoveZoom
190
+ * @property {(trackId?: string) => void} onNewTilesLoaded
191
+ * @property {(x: Scale, y: Scale) => void} onScalesChanged
192
+ * @property {import("pixi.js").Renderer} pixiRenderer
193
+ * @property {import("pixi.js").Container} pixiStage
194
+ * @property {Record<string, unknown>} pluginDataFetchers
195
+ * @property {Record<string, PluginTrack | MetaPluginTrack>} pluginTracks
196
+ * @property {Array<TrackDefinition>} positionedTracks
197
+ * @property {import('pub-sub-es').PubSub} pubSub
198
+ * @property {(func: SetCentersFunction) => void} setCentersFunction
199
+ * @property {HTMLElement} svgElement
200
+ * @property {string | typeof THEME_DARK} theme
201
+ * @property {number} topHeight
202
+ * @property {number} topHeightNoGallery
203
+ * @property {{ backgroundColor?: string }} viewOptions
204
+ * @property {number} width
205
+ * @property {[number, number]} xDomainLimits
206
+ * @property {[number, number]} yDomainLimits
207
+ * @property {boolean} valueScaleZoom
208
+ * @property {boolean} zoomable
209
+ * @property {[number, number]} zoomDomain
210
+ * @property {[number, number]} zoomLimits
211
+ * @property {string} uid
212
+ * @property {boolean} dragging
213
+ * @property {(func: (draggingStatus: boolean) => void) => void} registerDraggingChangedListener
214
+ * @property {boolean} disableTrackMenu
215
+ * @property {(listener: (draggingStatus: boolean) => void) => void} removeDraggingChangedListener
216
+ * @property {(trackId: string, tilesetInfo: TilesetInfo) => void} onTilesetInfoReceived
217
+ * @property {(trackId: string) => unknown} getLockGroupExtrema
218
+ * @property {(trackId: string) => boolean} isValueScaleLocked
219
+ * @property {(trackId: string) => void} onValueScaleChanged
220
+ * @property {(trackId: string, newOption: Record<string, unknown>) => void} onTrackOptionsChanged
221
+ */
222
+
223
+ /**
224
+ * @extends {React.Component<TrackRendererProps>}
225
+ */
88
226
  class TrackRenderer extends React.Component {
89
227
  /**
90
228
  * Maintain a list of tracks, and re-render them whenever either
@@ -93,29 +231,52 @@ class TrackRenderer extends React.Component {
93
231
  * Zooming changes the domain of the scales.
94
232
  *
95
233
  * Resizing changes the range. Both trigger a rerender.
234
+ *
235
+ * @param {TrackRendererProps} props
96
236
  */
97
237
  constructor(props) {
98
238
  super(props);
239
+ /** @type {boolean} */
99
240
  this.dragging = false; // is this element being dragged?
241
+ /** @type {WithZoomTransform<HTMLElement> | null} */
100
242
  this.element = null;
243
+ /** @type {HTMLElement | null} */
244
+ this.eventTracker = null;
245
+ /** @type {HTMLElement | null} */
246
+ this.eventTrackerOld = null;
247
+ /** @type {boolean} */
101
248
  this.closing = false;
102
249
 
250
+ /** @type {number} */
103
251
  this.yPositionOffset = 0;
252
+ /** @type {number} */
104
253
  this.xPositionOffset = 0;
254
+ /** @type {number} */
105
255
  this.scrollTop = 0;
106
256
 
257
+ /** @type {ReturnType<typeof setTimeout> | null} */
107
258
  this.scrollTimeout = null;
259
+ /** @type {number} */
108
260
  this.activeTransitions = 0;
109
261
 
262
+ /** @type {import('d3-zoom').ZoomTransform} */
110
263
  this.zoomTransform = zoomIdentity;
264
+ /** @type {() => void} */
111
265
  this.windowScrolledBound = this.windowScrolled.bind(this);
266
+ /** @type {(event?: import('d3-zoom').D3ZoomEvent<HTMLElement, unknown>) => void} */
112
267
  this.zoomStartedBound = this.zoomStarted.bind(this);
268
+ /** @type {(event: import('d3-zoom').D3ZoomEvent<HTMLElement, unknown> & { shiftKey?: boolean }) => void} */
113
269
  this.zoomedBound = this.zoomed.bind(this);
270
+ /** @type {() => void} */
114
271
  this.zoomEndedBound = this.zoomEnded.bind(this);
115
272
 
273
+ /** @type {string} */
116
274
  this.uid = slugid.nice();
275
+
276
+ /** @type {string} */
117
277
  this.viewUid = this.props.uid;
118
278
 
279
+ /** @type {unknown} */
119
280
  this.availableForPlugins = {
120
281
  ...AVAILABLE_FOR_PLUGINS,
121
282
  services: {
@@ -125,55 +286,72 @@ class TrackRenderer extends React.Component {
125
286
  },
126
287
  };
127
288
 
289
+ /** @type {boolean} */
128
290
  this.mounted = false;
129
291
 
130
292
  // create a zoom behavior that we'll just use to transform selections
131
293
  // without having it fire an "onZoom" event
294
+ /** @type {import("d3-zoom").ZoomBehavior<HTMLElement, unknown>} */
132
295
  this.emptyZoomBehavior = zoom();
133
296
 
134
297
  // a lot of the updates in TrackRenderer happen in response to
135
298
  // componentWillReceiveProps so we need to perform them with the
136
299
  // newest set of props. When cWRP is called, this.props still contains
137
300
  // the old props, so we need to store them in a new variable
301
+ /** @type {TrackRendererProps} */
138
302
  this.currentProps = props;
303
+ /** @type {string} */
139
304
  this.prevPropsStr = '';
140
305
 
141
306
  // catch any zooming behavior within all of the tracks in this plot
142
307
  // this.zoomTransform = zoomIdentity();
143
- this.zoomBehavior = zoom()
144
- .filter((event) => {
145
- if (event.target.classList.contains('no-zoom')) {
146
- return false;
147
- }
148
- if (event.target.classList.contains('react-resizable-handle')) {
149
- return false;
150
- }
151
- return true;
152
- })
153
- .on('start', this.zoomStartedBound)
154
- .on('zoom', this.zoomedBound)
155
- .on('end', this.zoomEndedBound);
308
+ /** @type {import("d3-zoom").ZoomBehavior<HTMLElement, unknown>} */
309
+ this.zoomBehavior =
310
+ /** @type {import("d3-zoom").ZoomBehavior<HTMLElement, any>} */ (zoom())
311
+ .filter((event) => {
312
+ if (event.target.classList.contains('no-zoom')) {
313
+ return false;
314
+ }
315
+ if (event.target.classList.contains('react-resizable-handle')) {
316
+ return false;
317
+ }
318
+ return true;
319
+ })
320
+ .on('start', this.zoomStartedBound)
321
+ .on('zoom', this.zoomedBound)
322
+ .on('end', this.zoomEndedBound);
156
323
 
324
+ /** @type {import('d3-zoom').ZoomTransform} */
157
325
  this.zoomTransform = zoomIdentity;
326
+ /** @type {import('d3-zoom').ZoomTransform} */
158
327
  this.prevZoomTransform = zoomIdentity;
159
328
 
329
+ /** @type {[number, number]} */
160
330
  this.initialXDomain = [0, 1];
331
+ /** @type {[number, number]} */
161
332
  this.initialYDomain = [0, 1];
333
+ /** @type {[number, number]} */
162
334
  this.xDomainLimits = [-Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER];
335
+ /** @type {[number, number]} */
163
336
  this.yDomainLimits = [-Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER];
337
+ /** @type {[number, number]} */
164
338
  this.zoomLimits = [0, Number.MAX_SAFE_INTEGER];
165
339
 
340
+ /** @type {number} */
166
341
  this.prevCenterX =
167
342
  this.currentProps.paddingLeft +
168
343
  this.currentProps.leftWidth +
169
344
  this.currentProps.centerWidth / 2;
345
+ /** @type {number} */
170
346
  this.prevCenterY =
171
347
  this.currentProps.paddingTop +
172
348
  this.currentProps.topHeight +
173
349
  this.currentProps.centerHeight / 2;
174
350
 
175
351
  // The offset of the center from the original. Used to keep the scales centered on resize events
352
+ /** @type {number} */
176
353
  this.cumCenterXOffset = 0;
354
+ /** @type {number} */
177
355
  this.cumCenterYOffset = 0;
178
356
 
179
357
  this.setUpInitialScales(
@@ -191,10 +369,13 @@ class TrackRenderer extends React.Component {
191
369
  // Each object will contain a trackDef
192
370
  // {'top': 100, 'left': 50,... 'track': {'source': 'http:...', 'type': 'heatmap'}}
193
371
  // And a trackObject which will be responsible for rendering it
372
+ /** @type {Record<string, { trackObject: TrackObject, trackDef: TrackDefinition }>} */
194
373
  this.trackDefObjects = {};
195
374
 
375
+ /** @type {Record<string, { trackObject: TrackObject | UnknownPixiTrack, trackDef: TrackConfig }>} */
196
376
  this.metaTracks = {};
197
377
 
378
+ /** @type {Array<import("pub-sub-es").Subscription>} */
198
379
  this.pubSubs = [];
199
380
 
200
381
  // if there's plugin tracks, they'll define new track
@@ -202,18 +383,43 @@ class TrackRenderer extends React.Component {
202
383
  // we look up the orientation of a track
203
384
  if (window.higlassTracksByType) {
204
385
  // Extend `TRACKS_INFO_BY_TYPE` with the configs of plugin tracks.
205
- Object.keys(window.higlassTracksByType).forEach((pluginTrackType) => {
386
+ for (const pluginTrackType in window.higlassTracksByType) {
206
387
  TRACKS_INFO_BY_TYPE[pluginTrackType] =
207
388
  window.higlassTracksByType[pluginTrackType].config;
208
- });
389
+ }
209
390
  }
210
391
 
392
+ /** @type {<T extends Event>(event: T & { sourceUid?: string, forwarded?: boolean }) => void} */
211
393
  this.boundForwardEvent = this.forwardEvent.bind(this);
394
+ /** @type {() => void} */
212
395
  this.boundScrollEvent = this.scrollEvent.bind(this);
396
+ /** @type {(event: { altKey: boolean, preventDefault(): void }) => void} */
213
397
  this.boundForwardContextMenu = this.forwardContextMenu.bind(this);
398
+ /** @type {(event: Event & { sourceUid: string, type: string }) => void} */
214
399
  this.dispatchEventBound = this.dispatchEvent.bind(this);
400
+ /** @type {(opts: { pos: [number, number, number, number], animateTime: number, isMercator: boolean }) => void} */
215
401
  this.zoomToDataPosHandlerBound = this.zoomToDataPosHandler.bind(this);
402
+ /** @type {(scrollTop: number) => void} */
216
403
  this.onScrollHandlerBound = this.onScrollHandler.bind(this);
404
+
405
+ /** @type {{ height: number, width: number, left: number, top: number }} */
406
+ this.elementPos = { height: 0, width: 0, left: 0, top: 0 };
407
+ /** @type {import('d3-selection').Selection<WithZoomTransform<HTMLElement>, unknown, null, unknown> | null} */
408
+ this.elementSelection = null;
409
+ }
410
+
411
+ get xScale() {
412
+ if (!this._xScale) {
413
+ throw new Error('xScale is not defined');
414
+ }
415
+ return this._xScale;
416
+ }
417
+
418
+ get yScale() {
419
+ if (!this._yScale) {
420
+ throw new Error('yScale is not defined');
421
+ }
422
+ return this._yScale;
217
423
  }
218
424
 
219
425
  // eslint-disable-next-line camelcase
@@ -237,13 +443,19 @@ class TrackRenderer extends React.Component {
237
443
  }
238
444
 
239
445
  componentDidMount() {
446
+ if (!this.element) {
447
+ throw new Error('Component did not mount, this.element is not defined.');
448
+ }
240
449
  this.elementPos = this.element.getBoundingClientRect();
241
450
  this.elementSelection = select(this.element);
242
- this.svgTrackAreaSelection = select(this.svgTrackArea);
243
451
 
452
+ /** @type {import('pixi.js').Graphics} */
244
453
  this.pStage = new GLOBALS.PIXI.Graphics();
454
+ /** @type {import('pixi.js').Graphics} */
245
455
  this.pMask = new GLOBALS.PIXI.Graphics();
456
+ /** @type {import('pixi.js').Graphics} */
246
457
  this.pOutline = new GLOBALS.PIXI.Graphics();
458
+ /** @type {import('pixi.js').Graphics} */
247
459
  this.pBackground = new GLOBALS.PIXI.Graphics();
248
460
 
249
461
  this.pStage.addChild(this.pMask);
@@ -274,6 +486,7 @@ class TrackRenderer extends React.Component {
274
486
  this.addEventTracker();
275
487
 
276
488
  // Init zoom and scale extent
489
+ /** @type {[[number, number], [number, number]]} */
277
490
  const transExt = [
278
491
  [this.xScale(this.xDomainLimits[0]), this.yScale(this.yDomainLimits[0])],
279
492
  [this.xScale(this.xDomainLimits[1]), this.yScale(this.yDomainLimits[1])],
@@ -281,6 +494,7 @@ class TrackRenderer extends React.Component {
281
494
 
282
495
  const svgBBox = this.svgElement.getBoundingClientRect();
283
496
 
497
+ /** @type {[[number, number], [number, number]]} */
284
498
  const ext = [
285
499
  [Math.max(transExt[0][0], 0), Math.max(transExt[0][1], 0)],
286
500
  [
@@ -296,6 +510,7 @@ class TrackRenderer extends React.Component {
296
510
  }
297
511
 
298
512
  // eslint-disable-next-line camelcase
513
+ /** @param {TrackRendererProps} nextProps */
299
514
  UNSAFE_componentWillReceiveProps(nextProps) {
300
515
  /**
301
516
  * The size of some tracks probably changed, so let's just
@@ -336,6 +551,7 @@ class TrackRenderer extends React.Component {
336
551
 
337
552
  this.svgElement = nextProps.svgElement;
338
553
 
554
+ /** @type {[[number, number], [number, number]]} */
339
555
  const transExt = [
340
556
  [this.xScale(this.xDomainLimits[0]), this.yScale(this.yDomainLimits[0])],
341
557
  [this.xScale(this.xDomainLimits[1]), this.yScale(this.yDomainLimits[1])],
@@ -343,6 +559,7 @@ class TrackRenderer extends React.Component {
343
559
 
344
560
  const svgBBox = this.svgElement.getBoundingClientRect();
345
561
 
562
+ /** @type {[[number, number], [number, number]]} */
346
563
  const ext = [
347
564
  [Math.max(transExt[0][0], 0), Math.max(transExt[0][1], 0)],
348
565
  [
@@ -365,7 +582,8 @@ class TrackRenderer extends React.Component {
365
582
  const trackObject = this.trackDefObjects[track.track.uid].trackObject;
366
583
  trackObject.rerender(options);
367
584
 
368
- if (track.track.hasOwnProperty('contents')) {
585
+ if (isCombinedTrackConfig(track.track)) {
586
+ /** @type {Record<string, TrackConfig>} */
369
587
  const ctDefs = {};
370
588
  for (const ct of track.track.contents) {
371
589
  ctDefs[ct.uid] = ct;
@@ -386,6 +604,7 @@ class TrackRenderer extends React.Component {
386
604
  }
387
605
  }
388
606
 
607
+ /** @param {TrackRendererProps} prevProps */
389
608
  componentDidUpdate(prevProps) {
390
609
  // If the initial domain changed, a new view config
391
610
  // probably has loaded. Reset the element's zoomTransform in this case.
@@ -396,7 +615,7 @@ class TrackRenderer extends React.Component {
396
615
  this.props.initialYDomain[0] !== prevProps.initialYDomain[0] ||
397
616
  this.props.initialYDomain[1] !== prevProps.initialYDomain[1]
398
617
  ) {
399
- this.element.__zoom = zoomIdentity;
618
+ if (this.element) this.element.__zoom = zoomIdentity;
400
619
  }
401
620
 
402
621
  if (prevProps.isRangeSelection !== this.props.isRangeSelection) {
@@ -427,10 +646,10 @@ class TrackRenderer extends React.Component {
427
646
  this.removeMetaTracks(Object.keys(this.metaTracks));
428
647
  this.currentProps.removeDraggingChangedListener(this.draggingChanged);
429
648
 
430
- this.currentProps.pixiStage.removeChild(this.pStage);
649
+ if (this.pStage) this.currentProps.pixiStage.removeChild(this.pStage);
431
650
 
432
- this.pMask.destroy(true);
433
- this.pStage.destroy(true);
651
+ this.pMask?.destroy(true);
652
+ this.pStage?.destroy(true);
434
653
 
435
654
  this.pubSubs.forEach((subscription) =>
436
655
  this.props.pubSub.unsubscribe(subscription),
@@ -445,24 +664,22 @@ class TrackRenderer extends React.Component {
445
664
  /**
446
665
  * Dispatch a forwarded event on the main DOM element
447
666
  *
448
- * @param {Object} e Event to be dispatched.
667
+ * @param {Event & { sourceUid: string, type: string }} event Event to be dispatched.
449
668
  */
450
- dispatchEvent(e) {
451
- if (e.sourceUid === this.uid && e.type !== 'contextmenu') {
452
- forwardEvent(e, this.element);
669
+ dispatchEvent(event) {
670
+ if (event.sourceUid === this.uid && event.type !== 'contextmenu') {
671
+ if (this.element) forwardEvent(event, this.element);
453
672
  }
454
673
  }
455
674
 
456
675
  /**
457
676
  * Check of a view position (i.e., pixel coords) is within this view
458
677
  *
459
- * @param {Number} x X position to be tested.
460
- * @param {Number} y Y position to be tested.
461
- * @return {Boolean} If `true` position is within this view.
678
+ * @param {number} x - X position to be tested.
679
+ * @param {number} y - Y position to be tested.
680
+ * @return {boolean} If `true` position is within this view.
462
681
  */
463
682
  isWithin(x, y) {
464
- if (!this.element) return false;
465
-
466
683
  const withinX =
467
684
  x >= this.elementPos.left &&
468
685
  x <= this.elementPos.width + this.elementPos.left;
@@ -473,8 +690,9 @@ class TrackRenderer extends React.Component {
473
690
  return withinX && withinY;
474
691
  }
475
692
 
476
- zoomToDataPosHandler({ pos, animateTime, isMercator }) {
477
- this.zoomToDataPos(...pos, animateTime, isMercator);
693
+ /** @param {{ pos: [number, number, number, number], animateTime: number }} opts */
694
+ zoomToDataPosHandler({ pos, animateTime }) {
695
+ this.zoomToDataPos(...pos, animateTime);
478
696
  }
479
697
 
480
698
  addZoom() {
@@ -496,15 +714,15 @@ class TrackRenderer extends React.Component {
496
714
  * don't overflow its bounds.
497
715
  */
498
716
  setMask() {
499
- this.pMask.clear();
500
- this.pMask.beginFill();
501
- this.pMask.drawRect(
717
+ this.pMask?.clear();
718
+ this.pMask?.beginFill();
719
+ this.pMask?.drawRect(
502
720
  this.xPositionOffset,
503
721
  this.yPositionOffset,
504
722
  this.currentProps.width,
505
723
  this.currentProps.height,
506
724
  );
507
- this.pMask.endFill();
725
+ this.pMask?.endFill();
508
726
 
509
727
  // show the bounds of this view
510
728
  /*
@@ -519,20 +737,18 @@ class TrackRenderer extends React.Component {
519
737
  setBackground() {
520
738
  const defBgColor = this.props.theme === THEME_DARK ? 'black' : 'white';
521
739
  const bgColor = colorToHex(
522
- (this.currentProps.viewOptions &&
523
- this.currentProps.viewOptions.backgroundColor) ||
524
- defBgColor,
740
+ this.currentProps.viewOptions?.backgroundColor ?? defBgColor,
525
741
  );
526
742
 
527
- this.pBackground.clear();
528
- this.pBackground.beginFill(bgColor);
529
- this.pBackground.drawRect(
743
+ this.pBackground?.clear();
744
+ this.pBackground?.beginFill(bgColor);
745
+ this.pBackground?.drawRect(
530
746
  this.xPositionOffset,
531
747
  this.yPositionOffset,
532
748
  this.currentProps.width,
533
749
  this.currentProps.height,
534
750
  );
535
- this.pBackground.endFill();
751
+ this.pBackground?.endFill();
536
752
  }
537
753
 
538
754
  windowScrolled() {
@@ -547,6 +763,13 @@ class TrackRenderer extends React.Component {
547
763
  }, SCROLL_TIMEOUT);
548
764
  }
549
765
 
766
+ /**
767
+ * @param {[number, number]} initialXDomain
768
+ * @param {[number, number]} initialYDomain
769
+ * @param {[number, number]} xDomainLimits
770
+ * @param {[number, number]} yDomainLimits
771
+ * @param {[number, number]} zoomLimits
772
+ */
550
773
  setUpInitialScales(
551
774
  initialXDomain = [0, 1],
552
775
  initialYDomain = [0, 1],
@@ -644,6 +867,7 @@ class TrackRenderer extends React.Component {
644
867
  this.currentProps.centerHeight / 2;
645
868
  }
646
869
 
870
+ /** @param {TrackRendererProps} props */
647
871
  updatablePropsToString(props) {
648
872
  return JSON.stringify({
649
873
  positionedTracks: props.positionedTracks,
@@ -660,6 +884,7 @@ class TrackRenderer extends React.Component {
660
884
  });
661
885
  }
662
886
 
887
+ /** @param {boolean} draggingStatus */
663
888
  draggingChanged(draggingStatus) {
664
889
  this.dragging = draggingStatus;
665
890
 
@@ -686,6 +911,10 @@ class TrackRenderer extends React.Component {
686
911
  // if the window is resized, we don't want to change the scale, but we do
687
912
  // want to move the center point. this needs to be tempered by the zoom
688
913
  // factor so that we keep the visible center point in the center
914
+ if (!this.drawableToDomainX || !this.drawableToDomainY) {
915
+ return;
916
+ }
917
+
689
918
  const centerDomainXOffset =
690
919
  (this.drawableToDomainX(currentCenterX) -
691
920
  this.drawableToDomainX(this.prevCenterX)) /
@@ -715,11 +944,11 @@ class TrackRenderer extends React.Component {
715
944
  // if the screen has been resized, then the domain width should remain the same
716
945
 
717
946
  // this.xScale should always span the region that the zoom behavior is being called on
718
- this.xScale = scaleLinear()
947
+ this._xScale = scaleLinear()
719
948
  .domain(visibleXDomain)
720
949
  .range([0, this.currentProps.width]);
721
950
 
722
- this.yScale = scaleLinear()
951
+ this._yScale = scaleLinear()
723
952
  .domain(visibleYDomain)
724
953
  .range([0, this.currentProps.height]);
725
954
 
@@ -736,6 +965,8 @@ class TrackRenderer extends React.Component {
736
965
 
737
966
  /**
738
967
  * Get a track's viewconf definition by its object
968
+ *
969
+ * @param {TrackObject} trackObjectIn
739
970
  */
740
971
  getTrackDef(trackObjectIn) {
741
972
  const trackDefItems = dictItems(this.trackDefObjects);
@@ -744,7 +975,7 @@ class TrackRenderer extends React.Component {
744
975
  if (trackObject === trackObjectIn) {
745
976
  return trackDef.track;
746
977
  }
747
- if (trackDef.track.contents) {
978
+ if (isCombinedTrackConfig(trackDef.track)) {
748
979
  // this is a combined track
749
980
  for (const subTrackDef of trackDef.track.contents) {
750
981
  if (trackObject.createdTracks[subTrackDef.uid] === trackObjectIn) {
@@ -757,8 +988,11 @@ class TrackRenderer extends React.Component {
757
988
  return null;
758
989
  }
759
990
 
760
- /*
991
+ /**
761
992
  * Fetch the trackObject for a track with a given ID
993
+ *
994
+ * @param {string} trackId
995
+ * @return {TrackObject | undefined}
762
996
  */
763
997
  getTrackObject(trackId) {
764
998
  const trackDefItems = dictItems(this.trackDefObjects);
@@ -817,6 +1051,9 @@ class TrackRenderer extends React.Component {
817
1051
  }
818
1052
  }
819
1053
 
1054
+ /**
1055
+ * @param {Array<TrackConfig>} trackDefinitions
1056
+ */
820
1057
  syncMetaTracks(trackDefinitions) {
821
1058
  const knownMetaTrackIds = Object.keys(this.metaTracks);
822
1059
  const newMetaTracks = new Set(trackDefinitions.map((def) => def.uid));
@@ -837,6 +1074,9 @@ class TrackRenderer extends React.Component {
837
1074
  );
838
1075
  }
839
1076
 
1077
+ /**
1078
+ * @param {Array<TrackDefinition>} trackDefinitions
1079
+ */
840
1080
  syncTrackObjects(trackDefinitions) {
841
1081
  /**
842
1082
  * Make sure we have a track object for every passed track definition.
@@ -858,6 +1098,7 @@ class TrackRenderer extends React.Component {
858
1098
  */
859
1099
  this.prevTrackDefinitions = JSON.stringify(trackDefinitions);
860
1100
 
1101
+ /** @type {Record<string, TrackDefinition>} */
861
1102
  const receivedTracksDict = {};
862
1103
  for (let i = 0; i < trackDefinitions.length; i++) {
863
1104
  receivedTracksDict[trackDefinitions[i].track.uid] = trackDefinitions[i];
@@ -899,7 +1140,7 @@ class TrackRenderer extends React.Component {
899
1140
  /**
900
1141
  * Add new meta tracks
901
1142
  *
902
- * @param {Array} metaTrackDefs Definitions of meta tracks to be added.
1143
+ * @param {Array<TrackConfig>} metaTrackDefs Definitions of meta tracks to be added.
903
1144
  */
904
1145
  addMetaTracks(metaTrackDefs) {
905
1146
  metaTrackDefs
@@ -912,6 +1153,9 @@ class TrackRenderer extends React.Component {
912
1153
  });
913
1154
  }
914
1155
 
1156
+ /**
1157
+ * @param {Array<TrackDefinition>} newTrackDefinitions
1158
+ */
915
1159
  addNewTracks(newTrackDefinitions) {
916
1160
  /**
917
1161
  * We need to create new track objects for the given track
@@ -919,12 +1163,16 @@ class TrackRenderer extends React.Component {
919
1163
  */
920
1164
  if (!this.currentProps.pixiStage) {
921
1165
  return;
922
- } // we need a pixi stage to start rendering
1166
+ }
1167
+ // we need a pixi stage to start rendering
923
1168
  // the parent component where it lives probably
924
1169
  // hasn't been mounted yet
925
1170
 
926
1171
  for (let i = 0; i < newTrackDefinitions.length; i++) {
927
1172
  const newTrackDef = newTrackDefinitions[i];
1173
+
1174
+ /** @type {TrackObject} */
1175
+ // @ts-expect-error - FIXME: Should not need to lie about the return type from createTrackObject.
928
1176
  const newTrackObj = this.createTrackObject(newTrackDef.track);
929
1177
 
930
1178
  // newTrackObj.refXScale(this.xScale);
@@ -949,21 +1197,23 @@ class TrackRenderer extends React.Component {
949
1197
  this.applyZoomTransform(false);
950
1198
  }
951
1199
 
952
- updateMetaTracks() {
1200
+ /** @param {unknown} _unused */
1201
+ updateMetaTracks(_unused) {
953
1202
  // Nothing
954
1203
  }
955
1204
 
1205
+ /** @param {Array<TrackDefinition>} newTrackDefs */
956
1206
  updateExistingTrackDefs(newTrackDefs) {
957
- for (let i = 0; i < newTrackDefs.length; i++) {
958
- this.trackDefObjects[newTrackDefs[i].track.uid].trackDef =
959
- newTrackDefs[i];
1207
+ for (const trackDef of newTrackDefs) {
1208
+ const ref = this.trackDefObjects[trackDef.track.uid];
1209
+ ref.trackDef = trackDef;
960
1210
 
961
1211
  // if it's a CombinedTrack, we have to see if its contents have changed
962
1212
  // e.g. somebody may have added a new Series
963
- if (newTrackDefs[i].track.type === 'combined') {
964
- this.trackDefObjects[newTrackDefs[i].track.uid].trackObject
1213
+ if (isCombinedTrackConfig(trackDef.track)) {
1214
+ ref.trackObject
965
1215
  .updateContents(
966
- newTrackDefs[i].track.contents,
1216
+ trackDef.track.contents,
967
1217
  this.createTrackObject.bind(this),
968
1218
  )
969
1219
  .refScalesChanged(this.xScale, this.yScale);
@@ -988,10 +1238,13 @@ class TrackRenderer extends React.Component {
988
1238
  const prevPosition = trackObject.position;
989
1239
  const prevDimensions = trackObject.dimensions;
990
1240
 
1241
+ /** @type {[number, number]} */
991
1242
  const newPosition = [
992
1243
  this.xPositionOffset + trackDef.left,
993
1244
  this.yPositionOffset + trackDef.top,
994
1245
  ];
1246
+
1247
+ /** @type {[number, number]} */
995
1248
  const newDimensions = [trackDef.width, trackDef.height];
996
1249
 
997
1250
  // check if any of the track's positions have changed
@@ -1024,14 +1277,17 @@ class TrackRenderer extends React.Component {
1024
1277
  return updated;
1025
1278
  }
1026
1279
 
1280
+ /** @param {string[]} trackIds */
1027
1281
  removeMetaTracks(trackIds) {
1028
1282
  trackIds.forEach((id) => {
1029
1283
  this.metaTracks[id].trackObject.remove();
1284
+ // @ts-expect-error - We are deleting the track object here
1030
1285
  this.metaTracks[id] = undefined;
1031
1286
  delete this.metaTracks[id];
1032
1287
  });
1033
1288
  }
1034
1289
 
1290
+ /** @param {string[]} trackUids */
1035
1291
  removeTracks(trackUids) {
1036
1292
  for (let i = 0; i < trackUids.length; i++) {
1037
1293
  this.trackDefObjects[trackUids[i]].trackObject.remove();
@@ -1047,10 +1303,10 @@ class TrackRenderer extends React.Component {
1047
1303
  * @param {boolean} notify If `true` notify listeners that the scales
1048
1304
  * have changed. This can be turned off to prevent circular updates when
1049
1305
  * scales are locked.
1050
- * @param {boolean} animate If `true` transition smoothly from the
1051
- * current to the desired location.
1052
1306
  * @param {number} animateTime Animation time in milliseconds. Only used
1053
1307
  * when `animate` is true.
1308
+ * @param {Scale} xScale The scale to use for the X axis.
1309
+ * @param {Scale} yScale The scale to use for the Y axis.
1054
1310
  */
1055
1311
  setCenter(
1056
1312
  centerX,
@@ -1080,6 +1336,7 @@ class TrackRenderer extends React.Component {
1080
1336
  const translateX = middleViewX - xScale(centerX) * k;
1081
1337
  const translateY = middleViewY - yScale(centerY) * k;
1082
1338
 
1339
+ /** @type {[Scale, Scale] | undefined} */
1083
1340
  let last;
1084
1341
 
1085
1342
  const setZoom = () => {
@@ -1088,18 +1345,21 @@ class TrackRenderer extends React.Component {
1088
1345
  .scale(k);
1089
1346
 
1090
1347
  this.zoomTransform = newTransform;
1091
- this.emptyZoomBehavior.transform(this.elementSelection, newTransform);
1348
+ if (this.elementSelection) {
1349
+ this.emptyZoomBehavior.transform(this.elementSelection, newTransform);
1350
+ }
1092
1351
 
1093
1352
  last = this.applyZoomTransform(notify);
1094
1353
  };
1095
1354
 
1096
- if (animateTime) {
1355
+ if (animateTime && this.elementSelection) {
1097
1356
  let selection = this.elementSelection;
1098
1357
 
1099
1358
  this.activeTransitions += 1;
1100
1359
 
1101
1360
  if (!document.hidden) {
1102
1361
  // only transition if the window is hidden
1362
+ // @ts-expect-error - Returns a TransitionSelection, which should be OK to use below
1103
1363
  selection = selection.transition().duration(animateTime);
1104
1364
  }
1105
1365
 
@@ -1119,21 +1379,37 @@ class TrackRenderer extends React.Component {
1119
1379
  return last;
1120
1380
  }
1121
1381
 
1382
+ /** @param {number} movement */
1122
1383
  valueScaleMove(movement) {
1384
+ if (!this.zoomStartPos) {
1385
+ return;
1386
+ }
1123
1387
  // mouse wheel from zoom event
1124
1388
  // const cp = pointer(event.sourceEvent, this.props.canvasElement);
1125
1389
  for (const track of this.getTracksAtPosition(...this.zoomStartPos)) {
1126
1390
  track.movedY(movement);
1127
1391
  }
1128
1392
 
1129
- this.zoomTransform = this.zoomStartTransform;
1393
+ if (this.zoomStartTransform) this.zoomTransform = this.zoomStartTransform;
1130
1394
  }
1131
1395
 
1396
+ /**
1397
+ * @param {{ sourceEvent: Event }} event
1398
+ * @param {string | null} orientation
1399
+ */
1132
1400
  valueScaleZoom(event, orientation) {
1133
1401
  // mouse move probably from a drag event
1402
+ if (!isWheelEvent(event.sourceEvent)) {
1403
+ return;
1404
+ }
1134
1405
  const mdy = event.sourceEvent.deltaY;
1135
1406
  const mdm = event.sourceEvent.deltaMode;
1136
1407
 
1408
+ /**
1409
+ * @param {number} dy
1410
+ * @param {number} dm
1411
+ * @return {number}
1412
+ */
1137
1413
  const myWheelDelta = (dy, dm) => (dy * (dm ? 120 : 1)) / 500;
1138
1414
  const mwd = myWheelDelta(mdy, mdm);
1139
1415
 
@@ -1148,7 +1424,7 @@ class TrackRenderer extends React.Component {
1148
1424
  }
1149
1425
 
1150
1426
  // reset the zoom transform
1151
- this.zoomTransform = this.zoomStartTransform;
1427
+ if (this.zoomStartTransform) this.zoomTransform = this.zoomStartTransform;
1152
1428
  }
1153
1429
 
1154
1430
  /**
@@ -1156,11 +1432,14 @@ class TrackRenderer extends React.Component {
1156
1432
  *
1157
1433
  * We need to update our local record of the zoom transform and apply it
1158
1434
  * to all the tracks.
1435
+ *
1436
+ * @param {import("d3-zoom").D3ZoomEvent<HTMLElement, unknown> & { shiftKey?: boolean }} event
1159
1437
  */
1160
1438
  zoomed(event) {
1161
1439
  // the orientation of the track where we started zooming
1162
1440
  // if it's a 1d-horizontal, then mousemove events shouldn't
1163
1441
  // move the center track vertically
1442
+ /** @type {string | null} */
1164
1443
  let trackOrientation = null;
1165
1444
 
1166
1445
  // see what orientation of track we're over so that we decide
@@ -1171,7 +1450,11 @@ class TrackRenderer extends React.Component {
1171
1450
  const trackAtZoomStart = tracksAtZoomStart[0];
1172
1451
  const trackDef = this.getTrackDef(trackAtZoomStart);
1173
1452
 
1174
- if (TRACKS_INFO_BY_TYPE[trackDef.type]) {
1453
+ if (!trackDef) {
1454
+ return;
1455
+ }
1456
+
1457
+ if (TRACKS_INFO_BY_TYPE[trackDef.type]?.orientation) {
1175
1458
  // some track types (like overlay-track don't have a track info)
1176
1459
  trackOrientation = TRACKS_INFO_BY_TYPE[trackDef.type].orientation;
1177
1460
  }
@@ -1231,8 +1514,7 @@ class TrackRenderer extends React.Component {
1231
1514
  .translate(this.prevZoomTransform.x, this.zoomTransform.y)
1232
1515
  .scale(this.zoomTransform.k);
1233
1516
  }
1234
-
1235
- this.element.__zoom = this.zoomTransform;
1517
+ if (this.element) this.element.__zoom = this.zoomTransform;
1236
1518
  }
1237
1519
 
1238
1520
  this.applyZoomTransform(true);
@@ -1250,13 +1532,15 @@ class TrackRenderer extends React.Component {
1250
1532
  *
1251
1533
  * The position should be relative to this.props.canvasElement.
1252
1534
  *
1253
- * @param {Number} x The query x position
1254
- * @param {Number} y The query y position
1255
- * @return {Array} An array of tracks at this position
1535
+ * @param {number} x The query x position
1536
+ * @param {number} y The query y position
1537
+ * @return {Array<TrackObject>} An array of tracks at this position
1256
1538
  */
1257
1539
  getTracksAtPosition(x, y) {
1540
+ /** @type {Array<TrackObject>} */
1258
1541
  const foundTracks = [];
1259
1542
 
1543
+ /** @type {Array<TrackObject>} */
1260
1544
  let tracksToVisit = [];
1261
1545
 
1262
1546
  for (const uid in this.trackDefObjects) {
@@ -1283,6 +1567,7 @@ class TrackRenderer extends React.Component {
1283
1567
  return foundTracks;
1284
1568
  }
1285
1569
 
1570
+ /** @param {import('d3-zoom').D3ZoomEvent<HTMLElement, unknown>=} event */
1286
1571
  zoomStarted(event) {
1287
1572
  this.zooming = true;
1288
1573
 
@@ -1307,18 +1592,26 @@ class TrackRenderer extends React.Component {
1307
1592
 
1308
1593
  if (this.valueScaleZooming) {
1309
1594
  this.valueScaleZooming = false;
1310
- this.element.__zoom = this.zoomStartTransform;
1595
+ if (this.element) this.element.__zoom = this.zoomStartTransform;
1311
1596
  }
1312
1597
 
1313
1598
  this.props.pubSub.publish('app.zoomEnd');
1314
1599
  }
1315
1600
 
1601
+ /**
1602
+ * @param {boolean=} notify
1603
+ * @returns {[Scale, Scale] | undefined}
1604
+ */
1316
1605
  applyZoomTransform(notify = true) {
1317
1606
  const props = this.currentProps;
1318
1607
  const paddingleft = props.paddingLeft + props.leftWidth;
1319
1608
  const paddingTop = props.paddingTop + props.topHeight;
1320
1609
 
1321
1610
  // These props are apparently used elsewhere, for example the context menu
1611
+ if (!this.xScale || !this.yScale) {
1612
+ return undefined;
1613
+ }
1614
+
1322
1615
  this.zoomedXScale = this.zoomTransform.rescaleX(this.xScale);
1323
1616
  this.zoomedYScale = this.zoomTransform.rescaleY(this.yScale);
1324
1617
 
@@ -1428,13 +1721,15 @@ class TrackRenderer extends React.Component {
1428
1721
  return [newXScale, newYScale];
1429
1722
  }
1430
1723
 
1724
+ /** @param {TrackConfig} track */
1431
1725
  createMetaTrack(track) {
1432
1726
  switch (track.type) {
1433
1727
  default: {
1434
1728
  // Check if a plugin track is available
1435
1729
  const pluginTrack = this.props.pluginTracks[track.type];
1436
1730
 
1437
- if (pluginTrack && pluginTrack.isMetaTrack) {
1731
+ if (pluginTrack?.isMetaTrack) {
1732
+ /** @type {MetaPluginTrackContext} */
1438
1733
  const context = {
1439
1734
  getTrackObject: this.getTrackObject.bind(this),
1440
1735
  onNewTilesLoaded: () => {
@@ -1459,15 +1754,15 @@ class TrackRenderer extends React.Component {
1459
1754
  }
1460
1755
 
1461
1756
  console.warn(`Unknown meta track of type: ${track.type}`);
1462
- return new UnknownPixiTrack(
1463
- this.pStage,
1464
- { name: 'Unknown Track Type', type: track.type },
1465
- () => this.currentProps.onNewTilesLoaded(track.uid),
1466
- );
1757
+ return new UnknownPixiTrack(this.pStage, {
1758
+ name: 'Unknown Track Type',
1759
+ type: track.type,
1760
+ });
1467
1761
  }
1468
1762
  }
1469
1763
  }
1470
1764
 
1765
+ /** @param {TrackConfig} track */
1471
1766
  createTrackObject(track) {
1472
1767
  const trackObject = this.createLocationAgnosticTrackObject(track);
1473
1768
  if (track.position === 'left' || track.position === 'right') {
@@ -1478,11 +1773,8 @@ class TrackRenderer extends React.Component {
1478
1773
  return trackObject;
1479
1774
  }
1480
1775
 
1776
+ /** @param {TrackConfig} track */
1481
1777
  createLocationAgnosticTrackObject(track) {
1482
- const handleTilesetInfoReceived = (x) => {
1483
- this.currentProps.onTilesetInfoReceived(track.uid, x);
1484
- };
1485
-
1486
1778
  // See if this track has a data config section.
1487
1779
  // If it doesn't, we assume that it has the standard
1488
1780
  // server / tilesetUid sections
@@ -1507,7 +1799,13 @@ class TrackRenderer extends React.Component {
1507
1799
  this.availableForPlugins,
1508
1800
  );
1509
1801
 
1802
+ // FIXME: non-null assert?
1803
+ if (!this.pStage || !this.svgElement) {
1804
+ throw new Error('No PIXI stage or svg element');
1805
+ }
1806
+
1510
1807
  // To simplify the context creation via ES6 object shortcuts.
1808
+ /** @type {PluginTrackContext} */
1511
1809
  const context = {
1512
1810
  id: track.uid,
1513
1811
  trackUid: track.uid,
@@ -1519,7 +1817,9 @@ class TrackRenderer extends React.Component {
1519
1817
  dataFetcher,
1520
1818
  getLockGroupExtrema: () =>
1521
1819
  this.currentProps.getLockGroupExtrema(track.uid),
1522
- handleTilesetInfoReceived,
1820
+ handleTilesetInfoReceived: (tilesetInfo) => {
1821
+ this.currentProps.onTilesetInfoReceived(track.uid, tilesetInfo);
1822
+ },
1523
1823
  animate: () => {
1524
1824
  this.currentProps.onNewTilesLoaded(track.uid);
1525
1825
  },
@@ -1627,7 +1927,7 @@ class TrackRenderer extends React.Component {
1627
1927
  context.setDomainsCallback = track.setDomainsCallback;
1628
1928
  return new ViewportTracker2D(context, options);
1629
1929
  }
1630
- return new Track(context, options);
1930
+ return new Track(context);
1631
1931
 
1632
1932
  case 'viewport-projection-horizontal':
1633
1933
  // TODO: Fix this so that these functions are defined somewhere else
@@ -1641,7 +1941,7 @@ class TrackRenderer extends React.Component {
1641
1941
  context.setDomainsCallback = track.setDomainsCallback;
1642
1942
  return new ViewportTrackerHorizontal(context, options);
1643
1943
  }
1644
- return new Track(context, options);
1944
+ return new Track(context);
1645
1945
 
1646
1946
  case 'viewport-projection-vertical':
1647
1947
  // TODO: Fix this so that these functions are defined somewhere else
@@ -1655,7 +1955,7 @@ class TrackRenderer extends React.Component {
1655
1955
  context.setDomainsCallback = track.setDomainsCallback;
1656
1956
  return new ViewportTrackerVertical(context, options);
1657
1957
  }
1658
- return new Track(context, options);
1958
+ return new Track(context);
1659
1959
 
1660
1960
  case 'gene-annotations':
1661
1961
  case 'horizontal-gene-annotations': // legacy, included for backwards compatiblity
@@ -1686,9 +1986,11 @@ class TrackRenderer extends React.Component {
1686
1986
  return new SquareMarkersTrack(context, options);
1687
1987
 
1688
1988
  case 'combined':
1989
+ // @ts-expect-error - FIXME: Our typing should be able to narrow track config
1990
+ // based on the type, but this isn't communicated in the type system yet.
1689
1991
  context.tracks = track.contents;
1690
1992
  context.createTrackObject = this.createTrackObject.bind(this);
1691
- return new CombinedTrack(context, options);
1993
+ return new CombinedTrack(context);
1692
1994
 
1693
1995
  case '2d-chromosome-labels':
1694
1996
  return new Chromosome2DLabels(context, options);
@@ -1818,7 +2120,7 @@ class TrackRenderer extends React.Component {
1818
2120
  * @param {number} dataYStart Data start Y coordinate.
1819
2121
  * @param {number} dataYEnd Data end Y coordinate.
1820
2122
  * @param {number} animateTime Animation time in milliseconds.
1821
- * @param {function} projector If not `null` a projector function that
2123
+ * @param {ProjectorFunction | null} projector If not `null` a projector function that
1822
2124
  * provides adjusted x and y scales.
1823
2125
  */
1824
2126
  zoomToDataPos(
@@ -1849,6 +2151,9 @@ class TrackRenderer extends React.Component {
1849
2151
  );
1850
2152
  }
1851
2153
 
2154
+ /**
2155
+ * @param {{ altKey: boolean, preventDefault: () => void }} e
2156
+ */
1852
2157
  forwardContextMenu(e) {
1853
2158
  // Do never forward the contextmenu event when ALT is being hold down.
1854
2159
  if (this.props.disableTrackMenu || e.altKey) return;
@@ -1958,15 +2263,24 @@ class TrackRenderer extends React.Component {
1958
2263
  }
1959
2264
 
1960
2265
  scrollEvent() {
2266
+ if (!this.element) return;
1961
2267
  this.elementPos = this.element.getBoundingClientRect();
1962
2268
  }
1963
2269
 
1964
- forwardEvent(e) {
1965
- e.sourceUid = this.uid;
1966
- e.forwarded = true;
1967
- this.props.pubSub.publish('app.event', e);
2270
+ /**
2271
+ * Publishes an event to the pubSub channel, first overriding the
2272
+ * sourceUid to be the uid of this track renderer.
2273
+ *
2274
+ * @template {Event} T
2275
+ * @param {T & { sourceUid?: string; forwarded?: boolean }} event
2276
+ */
2277
+ forwardEvent(event) {
2278
+ event.sourceUid = this.uid;
2279
+ event.forwarded = true;
2280
+ this.props.pubSub.publish('app.event', event);
1968
2281
  }
1969
2282
 
2283
+ /** @param {number} scrollTop */
1970
2284
  onScrollHandler(scrollTop) {
1971
2285
  this.scrollTop = scrollTop;
1972
2286
  }
@@ -2072,6 +2386,8 @@ TrackRenderer.propTypes = {
2072
2386
  valueScaleZoom: PropTypes.bool,
2073
2387
  zoomable: PropTypes.bool.isRequired,
2074
2388
  zoomDomain: PropTypes.array,
2389
+ uid: PropTypes.string,
2390
+ zoomLimits: PropTypes.array,
2075
2391
  };
2076
2392
 
2077
2393
  export default withPubSub(withTheme(TrackRenderer));