gridviz 3.0.41 → 3.1.1

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 (48) hide show
  1. package/README.md +18 -15
  2. package/dist/gridviz.js +3227 -1941
  3. package/dist/gridviz.min.js +1 -1
  4. package/package.json +6 -4
  5. package/src/button/Button.js +77 -68
  6. package/src/button/FullscreenButton.js +123 -123
  7. package/src/button/ZoomButtons.js +109 -104
  8. package/src/core/Drawable.js +25 -3
  9. package/src/core/GeoCanvas.js +15 -0
  10. package/src/core/Legend.js +267 -245
  11. package/src/core/Map.js +180 -30
  12. package/src/core/Tooltip.js +191 -185
  13. package/src/dataset/TiledGrid.js +74 -24
  14. package/src/index.js +82 -79
  15. package/src/layer/GridLayer.js +196 -194
  16. package/src/layer/LabelLayer.js +7 -4
  17. package/src/legend/ColorCategoryLegend.js +127 -125
  18. package/src/legend/ColorDiscreteLegend.js +116 -114
  19. package/src/legend/ColorLegend.js +33 -11
  20. package/src/legend/OrientationLegend.js +94 -92
  21. package/src/legend/SizeLegend.js +8 -1
  22. package/src/legend/TernaryLegend.js +317 -314
  23. package/src/legend/VennLegend.js +202 -0
  24. package/src/style/CircleColorSizeWebGLStyle.js +249 -0
  25. package/src/style/CompositionStyle.js +1 -1
  26. package/src/style/DotDensityStyle.js +8 -4
  27. package/src/style/ImageStyle.js +1 -1
  28. package/src/style/Interpolator.js +31 -6
  29. package/src/style/IsoFenceStyle.js +1 -1
  30. package/src/style/LegoStyle.js +6 -5
  31. package/src/style/MosaicStyle.js +1 -1
  32. package/src/style/NinjaStarStyle.js +1 -1
  33. package/src/style/PillarStyle.js +1 -1
  34. package/src/style/RandomWalkInterpolator.js +213 -0
  35. package/src/style/ShadingRayStyle.js +16 -2
  36. package/src/style/ShadingStyle.js +80 -72
  37. package/src/style/ShapeColorSizeStyle.js +18 -2
  38. package/src/style/SideCategoryStyle.js +1 -1
  39. package/src/style/SideStyle.js +1 -1
  40. package/src/style/SideTanakaStyle.js +1 -1
  41. package/src/style/SquareColorCategoryWebGLStyle.js +71 -20
  42. package/src/style/SquareColorWebGLStyle.js +7 -4
  43. package/src/style/StrokeStyle.js +1 -1
  44. package/src/style/TextStyle.js +1 -1
  45. package/src/style/TimeSeriesStyle.js +14 -7
  46. package/src/utils/WebGLSquareColoringAdvanced.js +47 -9
  47. package/src/utils/scale.js +20 -0
  48. package/src/utils/webGLUtils.js +52 -0
package/src/core/Map.js CHANGED
@@ -2,6 +2,7 @@
2
2
  'use strict'
3
3
 
4
4
  // internal imports
5
+ import { isVisibleAt } from './Drawable.js'
5
6
  import { GeoCanvas } from './GeoCanvas.js'
6
7
  import { Tooltip } from './Tooltip.js'
7
8
  import { ZoomButtons } from '../button/ZoomButtons.js'
@@ -10,6 +11,14 @@ import { FullscreenButton } from '../button/FullscreenButton.js'
10
11
  // external imports
11
12
  import { select } from 'd3-selection'
12
13
 
14
+ /**
15
+ * Tells a cell from another across redraws, which can make the cells again: by position and resolution.
16
+ * @param {{cell:{x:any,y:any},resolution:number}} focus
17
+ * @returns {string} */
18
+ function cellKey(focus) {
19
+ return focus.cell.x + ',' + focus.cell.y + ',' + focus.resolution
20
+ }
21
+
13
22
  /**
14
23
  * A gridviz application.
15
24
  *
@@ -31,12 +40,12 @@ export class Map {
31
40
  this.layers = opts.layers || []
32
41
 
33
42
  /**
34
- * When true, redraw() schedules one redraw for the next animation frame instead of
35
- * drawing at once, so the many redraw requests of a busy moment (every tile that
36
- * arrives, every info.json, every setting change) are drawn once. redrawNow() still
37
- * draws at once. Off by default: redraw() stays synchronous, as it always was.
43
+ * When true (the default), redraw() schedules one redraw for the next animation frame
44
+ * instead of drawing at once, so the many redraw requests of a busy moment (every tile
45
+ * that arrives, every info.json, every setting change) are drawn once. redrawNow() still
46
+ * draws at once. Set it to false for redraw() to be synchronous, as it was before 3.1.0.
38
47
  * @type {boolean} */
39
- this.asyncRedraw = !!opts.asyncRedraw
48
+ this.asyncRedraw = opts.asyncRedraw !== false
40
49
  /** @private @type {number|null} */
41
50
  this._redrawFrame = null
42
51
 
@@ -121,7 +130,49 @@ export class Map {
121
130
  this._focusFrame = null
122
131
  /** @private */
123
132
  this._focusEvent = null
133
+ /** The last mouse event over the canvas, null when the mouse is out of it (see redrawNow)
134
+ * @private
135
+ * @type {MouseEvent|null} */
136
+ this._lastMouseEvent = null
137
+ /** True while the mouse is on a cell, which shows its rectangle and tooltip: the map gives them back after a redraw
138
+ * @private */
139
+ this._hovering = false
140
+ /** The cell the hover is on (see cellKey), to tell after a redraw if the mouse is still on the same one
141
+ * @private
142
+ * @type {string|null} */
143
+ this._hoverKey = null
144
+ /** @private */
145
+ this._refocusing = false
146
+ /** The layer and the dataset of the cell which is hovered (see _hoverKey): another layer, or dataset, at the same place is
147
+ * another cell, whose info is not the one of the tooltip (a time slider which changes the data for example)
148
+ * @private
149
+ * @type {{layer:*, dataset:*}|null} */
150
+ this._hoverSource = null
151
+ /** The cell whose info was asked for after a redraw, so that it is not asked again at each redraw
152
+ * @private
153
+ * @type {string|null} */
154
+ this._triedKey = null
155
+ /** @private
156
+ * @type {{layer:*, dataset:*}|null} */
157
+ this._triedSource = null
158
+ /** The last position of the mouse in the page. The canvas does not get the mouse events while the map is dragged or
159
+ * scrolled (the zoom of the canvas takes them), so this one is listened to in the window: the map shows the hover again
160
+ * where the mouse is when a redraw ends the pan or the zoom.
161
+ * @private
162
+ * @type {{clientX:number, clientY:number}|null} */
163
+ this._pointer = null
164
+ this._pointerHandler = (e) => {
165
+ this._pointer = { clientX: e.clientX, clientY: e.clientY }
166
+ }
167
+ // the mouse left the page
168
+ this._pointerLeaveHandler = (e) => {
169
+ if (!e.relatedTarget) this._pointer = null
170
+ }
171
+ for (const type of ['mousemove', 'mousedown', 'wheel'])
172
+ window.addEventListener(type, this._pointerHandler, { capture: true, passive: true })
173
+ window.addEventListener('mouseout', this._pointerLeaveHandler, { capture: true, passive: true })
124
174
  const scheduleFocus = (e) => {
175
+ this._lastMouseEvent = e
125
176
  this._focusEvent = e
126
177
  if (this._focusFrame) return
127
178
  this._focusFrame = requestAnimationFrame(() => {
@@ -137,6 +188,12 @@ export class Map {
137
188
  if (this._focusFrame) cancelAnimationFrame(this._focusFrame)
138
189
  this._focusFrame = null
139
190
  this._focusEvent = null
191
+ this._lastMouseEvent = null
192
+ this._hovering = false
193
+ this._hoverKey = null
194
+ this._hoverSource = null
195
+ this._triedKey = null
196
+ this._triedSource = null
140
197
  this.tooltip.hide()
141
198
  }
142
199
  this.geoCanvas.canvas.addEventListener('mouseover', this.mouseOverHandler)
@@ -149,6 +206,11 @@ export class Map {
149
206
  // add extra logic to onZoomStartFun
150
207
  this.geoCanvas.onZoomStartFun = (e) => {
151
208
  if (opts.onZoomStartFun) opts.onZoomStartFun(e)
209
+ this._hovering = false
210
+ this._hoverKey = null
211
+ this._hoverSource = null
212
+ this._triedKey = null
213
+ this._triedSource = null
152
214
  this.tooltip.hide()
153
215
  }
154
216
 
@@ -192,10 +254,10 @@ export class Map {
192
254
  .style('height', 'auto')
193
255
  .style('background', '#FFFFFF')
194
256
  //.style("padding", this.padding)
195
- .style('border', '0px')
196
- //.style('border-radius', '5px')
197
- .style('box-shadow', '3px 3px 3px grey, -3px -3px 3px #ddd')
198
- .style('font-family', 'Helvetica, Arial, sans-serif')
257
+ .style('border', '1px solid #d5d5d5')
258
+ .style('border-radius', '6px')
259
+ .style('box-shadow', '0px 4px 6px rgba(0, 0, 0, 0.1), 0px 1px 3px rgba(0, 0, 0, 0.08)')
260
+ .style('font-family', "'Helvetica Neue', Arial, Helvetica, sans-serif")
199
261
  .style('bottom', '15px')
200
262
  .style('right', '15px')
201
263
  //hide
@@ -263,7 +325,7 @@ export class Map {
263
325
  //go through the layers
264
326
  for (const layer of this.layers) {
265
327
  //check if layer is visible
266
- if (layer.visible && !layer.visible(z)) continue
328
+ if (!isVisibleAt(layer, z)) continue
267
329
 
268
330
  //set layer alpha and blend mode
269
331
  if (layer.alpha || layer.blendOperation) {
@@ -293,6 +355,23 @@ export class Map {
293
355
  // the canvas holds no focus rectangle any more
294
356
  this._focusedCell = undefined
295
357
 
358
+ // The mouse is on a cell: show its rectangle and its tooltip, in this very draw. The redraw cleared the rectangle,
359
+ // which would stay away until the next mouse move: a flicker when the hover redraws the map (#174), or when the
360
+ // map is scrolled or panned with the mouse over it. The cellInfoHTML of the layers is not called again for the cell
361
+ // which is hovered already: it can ask for a redraw (to show the cells around the one hovered for example), which
362
+ // would redraw for ever.
363
+ if (!this._refocusing && !this.geoCanvas._isZooming) {
364
+ const e = this.pointerEvent() || (this._hovering ? this._lastMouseEvent : null)
365
+ if (e) {
366
+ this._refocusing = true
367
+ try {
368
+ this.refocusCell(e)
369
+ } finally {
370
+ this._refocusing = false
371
+ }
372
+ }
373
+ }
374
+
296
375
  // the legends are in the container now
297
376
  if (this.onLegendUpdate && this.legend) this.onLegendUpdate(this.legend.node())
298
377
 
@@ -308,22 +387,89 @@ export class Map {
308
387
  return this.geoCanvas.updateExtentGeo(marginPx)
309
388
  }
310
389
 
311
- /** @param {MouseEvent} e */
312
- focusCell(e) {
390
+ /**
391
+ * The geographical position of a mouse event, with the offset of the tooltip.
392
+ * @param {MouseEvent} e
393
+ * @returns {{x:number,y:number}}
394
+ * @private */
395
+ mousePositionGeo(e) {
396
+ return {
397
+ x: this.geoCanvas.pixToGeoX(e.offsetX + this.tooltip.xMouseOffset),
398
+ y: this.geoCanvas.pixToGeoY(e.offsetY + this.tooltip.yMouseOffset),
399
+ }
400
+ }
401
+
402
+ /**
403
+ * The mouse event of the position of the mouse, when it is over the map: in the canvas, and not under something else of the
404
+ * page (a panel, a button...). Null when the position of the mouse is not known, or not over the map.
405
+ * @returns {{offsetX:number, offsetY:number, clientX:number, clientY:number}|null}
406
+ * @private */
407
+ pointerEvent() {
408
+ const p = this._pointer
409
+ if (!p) return null
410
+ const canvas = this.geoCanvas.canvas
411
+ const r = canvas.getBoundingClientRect()
412
+ if (p.clientX < r.left || p.clientX >= r.right || p.clientY < r.top || p.clientY >= r.bottom) return null
413
+ // (the root of the canvas is the document, or a shadow root when the map is in one)
414
+ const root = canvas.getRootNode ? canvas.getRootNode() : document
415
+ const top = (root.elementFromPoint ? root : document).elementFromPoint(p.clientX, p.clientY)
416
+ if (top !== canvas) return null
417
+ return { offsetX: p.clientX - r.left, offsetY: p.clientY - r.top, clientX: p.clientX, clientY: p.clientY }
418
+ }
419
+
420
+ /**
421
+ * Shows again the hover of the mouse position after a redraw cleared it. When the mouse is still on the cell it was on,
422
+ * its rectangle is drawn without asking its info again: the tooltip has it already, and cellInfoHTML is not called.
423
+ * The info of another cell is asked for once.
424
+ * @param {{offsetX:number, offsetY:number, clientX:number, clientY:number}} e
425
+ * @private */
426
+ refocusCell(e) {
427
+ if (this.geoCanvas._isZooming) return
428
+ const same = this.getCellFocusInfo(this.mousePositionGeo(e), false)
429
+ if (!same) {
430
+ // no cell here: nothing to show. Clear the hover which was, if any.
431
+ this._triedKey = null
432
+ this._triedSource = null
433
+ if (this._hovering) this.focusCell(e)
434
+ return
435
+ }
436
+ const key = cellKey(same)
437
+ const source = { layer: same.layer, dataset: same.dataset }
438
+ const sameSource = (o) => o && o.layer === source.layer && o.dataset === source.dataset
439
+ if (key === this._hoverKey && sameSource(this._hoverSource)) this.focusCell(e, same)
440
+ else if (!(key === this._triedKey && sameSource(this._triedSource))) {
441
+ // another cell (the map moved, or the cells did, or the data): its info, once. When it has none the cell is not
442
+ // hovered, and not asked again at the next redraw (until the mouse moves, the hover is ended by a pan, a zoom,
443
+ // or the data changes).
444
+ this.focusCell(e)
445
+ this._triedKey = key
446
+ this._triedSource = source
447
+ }
448
+ }
449
+
450
+ /**
451
+ * @param {MouseEvent} e
452
+ * @param {{cell:import('./Dataset.js').Cell,html?:string,resolution:number}} [known] The cell under the mouse, when already found.
453
+ * @returns {void} */
454
+ focusCell(e, known) {
313
455
  // Don’t process hover events during active zoom
314
456
  if (this.geoCanvas._isZooming) return;
315
457
 
316
- //compute mouse geo position
317
- const mousePositionGeo = {
318
- x: this.geoCanvas.pixToGeoX(e.offsetX + this.tooltip.xMouseOffset),
319
- y: this.geoCanvas.pixToGeoY(e.offsetY + this.tooltip.yMouseOffset),
458
+ // the mouse moved (or a redraw gave its cell, `known`): the info of the cells is asked again
459
+ if (!known && !this._refocusing) {
460
+ this._triedKey = null
461
+ this._triedSource = null
320
462
  }
321
- /** @type {{cell:import('./Dataset.js').Cell,html:string,resolution:number} | undefined} */
322
- const focus = this.getCellFocusInfo(mousePositionGeo)
463
+
464
+ /** @type {{cell:import('./Dataset.js').Cell,html?:string,resolution:number} | undefined} */
465
+ const focus = known || this.getCellFocusInfo(this.mousePositionGeo(e))
323
466
 
324
467
  // Same cell as the one already highlighted (or still no cell): the canvas is right
325
468
  // as it is, only the tooltip follows the mouse. Saves a full-canvas copy per move.
326
469
  const focusedCell = focus ? focus.cell : null
470
+ this._hovering = !!focus
471
+ this._hoverKey = focus ? cellKey(focus) : null
472
+ this._hoverSource = focus ? { layer: focus.layer, dataset: focus.dataset } : null
327
473
  if (focusedCell === this._focusedCell) {
328
474
  if (focus) {
329
475
  this.tooltip.setPosition(e)
@@ -341,7 +487,7 @@ export class Map {
341
487
  this.geoCanvas.ctx.drawImage(this.geoCanvas.offscreenCanvas, 0, 0);
342
488
 
343
489
  if (focus) {
344
- this.tooltip.html(focus.html);
490
+ if (focus.html !== undefined) this.tooltip.html(focus.html);
345
491
  this.tooltip.setPosition(e);
346
492
  this.tooltip.show();
347
493
 
@@ -375,7 +521,7 @@ export class Map {
375
521
 
376
522
  if (focus) {
377
523
  this.geoCanvas.canvas.style.cursor = 'pointer';
378
- this.tooltip.html(focus.html)
524
+ if (focus.html !== undefined) this.tooltip.html(focus.html)
379
525
  this.tooltip.setPosition(e)
380
526
  this.tooltip.show()
381
527
 
@@ -419,10 +565,11 @@ export class Map {
419
565
  * This is usefull for user interactions, to show this info where the user clicks for example.
420
566
  *
421
567
  * @param {{x:number,y:number}} posGeo
422
- * @returns {{cell:import('./Dataset.js').Cell,html:string,resolution:number} | undefined}
568
+ * @param {boolean} [withHtml] False to find the cell without calling the cellInfoHTML of its layer: no html then.
569
+ * @returns {{cell:import('./Dataset.js').Cell,html?:string,resolution:number} | undefined}
423
570
  * @protected
424
571
  */
425
- getCellFocusInfo(posGeo) {
572
+ getCellFocusInfo(posGeo, withHtml = true) {
426
573
 
427
574
  // go through the layers, starting from top one
428
575
  const z = this.geoCanvas.view.z
@@ -431,7 +578,7 @@ export class Map {
431
578
  /** @type {import("./Layer.js").Layer} */
432
579
  const layer = this.layers[i]
433
580
 
434
- if (layer.visible && !layer.visible(z)) continue
581
+ if (!isVisibleAt(layer, z)) continue
435
582
  if (layer.cellInfoHTML === 'none') continue
436
583
  if (!layer.cellInfoHTML) continue
437
584
  if (!layer.getDataset) continue
@@ -454,14 +601,14 @@ export class Map {
454
601
  //rare case for a dataset with mixed resolutions
455
602
  if (dsc.mixedResolution) {
456
603
  const r = +dsc.mixedResolution(cell)
457
- const html = layer.cellInfoHTML(cell, r)
458
- if (!html) return undefined
459
- return { cell: cell, html: html, resolution: r }
604
+ const html = withHtml ? layer.cellInfoHTML(cell, r) : undefined
605
+ if (withHtml && !html) return undefined
606
+ return { cell: cell, html: html, resolution: r, layer: layer, dataset: dsc }
460
607
  }
461
608
 
462
- const html = layer.cellInfoHTML(cell, dsc.getResolution())
463
- if (!html) return undefined
464
- return { cell: cell, html: html, resolution: dsc.getResolution() }
609
+ const html = withHtml ? layer.cellInfoHTML(cell, dsc.getResolution()) : undefined
610
+ if (withHtml && !html) return undefined
611
+ return { cell: cell, html: html, resolution: dsc.getResolution(), layer: layer, dataset: dsc }
465
612
  }
466
613
  }
467
614
 
@@ -649,7 +796,10 @@ export class Map {
649
796
  if (this._resizeObserver) this._resizeObserver.disconnect()
650
797
  this._resizeObserver = null
651
798
 
652
- // remove event listeners, from the canvas they were added to
799
+ // remove event listeners, from the window and from the canvas they were added to
800
+ for (const type of ['mousemove', 'mousedown', 'wheel'])
801
+ window.removeEventListener(type, this._pointerHandler, { capture: true })
802
+ window.removeEventListener('mouseout', this._pointerLeaveHandler, { capture: true })
653
803
  this.geoCanvas.canvas.removeEventListener('mouseover', this.mouseOverHandler)
654
804
  this.geoCanvas.canvas.removeEventListener('mousemove', this.mouseMoveHandler)
655
805
  this.geoCanvas.canvas.removeEventListener('mouseout', this.mouseOutHandler)