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.
- package/README.md +18 -15
- package/dist/gridviz.js +3227 -1941
- package/dist/gridviz.min.js +1 -1
- package/package.json +6 -4
- package/src/button/Button.js +77 -68
- package/src/button/FullscreenButton.js +123 -123
- package/src/button/ZoomButtons.js +109 -104
- package/src/core/Drawable.js +25 -3
- package/src/core/GeoCanvas.js +15 -0
- package/src/core/Legend.js +267 -245
- package/src/core/Map.js +180 -30
- package/src/core/Tooltip.js +191 -185
- package/src/dataset/TiledGrid.js +74 -24
- package/src/index.js +82 -79
- package/src/layer/GridLayer.js +196 -194
- package/src/layer/LabelLayer.js +7 -4
- package/src/legend/ColorCategoryLegend.js +127 -125
- package/src/legend/ColorDiscreteLegend.js +116 -114
- package/src/legend/ColorLegend.js +33 -11
- package/src/legend/OrientationLegend.js +94 -92
- package/src/legend/SizeLegend.js +8 -1
- package/src/legend/TernaryLegend.js +317 -314
- package/src/legend/VennLegend.js +202 -0
- package/src/style/CircleColorSizeWebGLStyle.js +249 -0
- package/src/style/CompositionStyle.js +1 -1
- package/src/style/DotDensityStyle.js +8 -4
- package/src/style/ImageStyle.js +1 -1
- package/src/style/Interpolator.js +31 -6
- package/src/style/IsoFenceStyle.js +1 -1
- package/src/style/LegoStyle.js +6 -5
- package/src/style/MosaicStyle.js +1 -1
- package/src/style/NinjaStarStyle.js +1 -1
- package/src/style/PillarStyle.js +1 -1
- package/src/style/RandomWalkInterpolator.js +213 -0
- package/src/style/ShadingRayStyle.js +16 -2
- package/src/style/ShadingStyle.js +80 -72
- package/src/style/ShapeColorSizeStyle.js +18 -2
- package/src/style/SideCategoryStyle.js +1 -1
- package/src/style/SideStyle.js +1 -1
- package/src/style/SideTanakaStyle.js +1 -1
- package/src/style/SquareColorCategoryWebGLStyle.js +71 -20
- package/src/style/SquareColorWebGLStyle.js +7 -4
- package/src/style/StrokeStyle.js +1 -1
- package/src/style/TextStyle.js +1 -1
- package/src/style/TimeSeriesStyle.js +14 -7
- package/src/utils/WebGLSquareColoringAdvanced.js +47 -9
- package/src/utils/scale.js +20 -0
- 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
|
|
35
|
-
* drawing at once, so the many redraw requests of a busy moment (every tile
|
|
36
|
-
* arrives, every info.json, every setting change) are drawn once. redrawNow() still
|
|
37
|
-
* draws at once.
|
|
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 =
|
|
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', '
|
|
196
|
-
|
|
197
|
-
.style('box-shadow', '
|
|
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
|
|
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
|
-
/**
|
|
312
|
-
|
|
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
|
-
//
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
322
|
-
|
|
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
|
-
* @
|
|
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
|
|
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)
|