gridviz 3.0.17 → 3.0.19

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gridviz",
3
- "version": "3.0.17",
3
+ "version": "3.0.19",
4
4
  "description": "Visualization tool for gridded statistics",
5
5
  "keywords": [
6
6
  "statistics",
@@ -46,7 +46,7 @@ export class Button {
46
46
  this.style('display', 'flex')
47
47
  this.style('justify-content', 'center')
48
48
  this.style('align-items', 'center')
49
- this.style('width', '30px')
49
+ this.style('width', '35px')
50
50
  this.style('height', '30px')
51
51
  // this.style(padding , '4px'
52
52
 
@@ -78,44 +78,24 @@ export class GeoCanvas {
78
78
  this.extGeo = { xMin: NaN, xMax: NaN, yMin: NaN, yMax: NaN }
79
79
  this.updateExtentGeo()
80
80
 
81
- //rely on d3 zoom for pan/zoom
81
+ //rely on d3 for zoom
82
82
  if (!opts.disableZoom) {
83
83
  let tP = zoomIdentity
84
+ // @ts-ignore
85
+ let debounceTimeout = null // Add a debounce timeout variable
84
86
  const z = d3zoom()
85
- //to make the zooming a bit faster
87
+ // to make the zooming a bit faster
86
88
  .wheelDelta((e) => -e.deltaY * (e.deltaMode === 1 ? 0.07 : e.deltaMode ? 1 : 0.004))
87
89
  .on('zoom', (e) => {
88
90
  const t = e.transform
89
- const f = tP.k / t.k
90
- if (f == 1) {
91
+ const zoomFactor = tP.k / t.k
92
+ if (zoomFactor == 1) {
91
93
  //pan
92
94
  const dx = tP.x - t.x
93
95
  const dy = tP.y - t.y
94
96
  this.pan(dx * this.view.z, -dy * this.view.z)
95
97
  } else {
96
- const se = e.sourceEvent
97
- if (se instanceof WheelEvent) {
98
- //zoom at the mouse position
99
- this.zoom(
100
- f,
101
- this.pixToGeoX(e.sourceEvent.offsetX),
102
- this.pixToGeoY(e.sourceEvent.offsetY)
103
- )
104
- } else if (se instanceof TouchEvent) {
105
- if (!se.targetTouches.length) return
106
- //pinch zoom
107
- //compute average position of the touches
108
- let tx = 0,
109
- ty = 0
110
- for (let tt of se.targetTouches) {
111
- tx += tt.clientX
112
- ty += tt.clientY
113
- }
114
- tx /= se.targetTouches.length
115
- ty /= se.targetTouches.length
116
- //zoom at this average position
117
- this.zoom(f, this.pixToGeoX(tx), this.pixToGeoY(ty))
118
- }
98
+ handleZoom(e, zoomFactor)
119
99
  }
120
100
  tP = t
121
101
 
@@ -134,13 +114,48 @@ export class GeoCanvas {
134
114
  if (this.onZoomStartFun) this.onZoomStartFun(e)
135
115
  })
136
116
  .on('end', (e) => {
137
- // end of zoom event
117
+ // end of pan/zoom event
138
118
  this.redraw()
139
119
  this.canvasSave = { c: null, dx: 0, dy: 0, f: 1 }
140
120
 
141
121
  if (this.onZoomEndFun) this.onZoomEndFun(e)
142
122
  })
123
+ // @ts-ignore
143
124
  z(select(this.canvas))
125
+
126
+ const handleZoom = (event, zoomFactor) => {
127
+ // cancel ongoing data requests
128
+ this.cancelCurrentRequests()
129
+ const se = event.sourceEvent
130
+
131
+ if (se instanceof WheelEvent) {
132
+ //zoom at the mouse position
133
+ this.zoom(
134
+ zoomFactor,
135
+ // @ts-ignore
136
+ this.pixToGeoX(se.offsetX),
137
+ // @ts-ignore
138
+ this.pixToGeoY(se.offsetY)
139
+ )
140
+ } else if (se instanceof TouchEvent) {
141
+ //compute average position of the touches
142
+ let tx = 0,
143
+ ty = 0
144
+ for (let tt of se.targetTouches) {
145
+ tx += tt.clientX
146
+ ty += tt.clientY
147
+ }
148
+ tx /= se.targetTouches.length
149
+ ty /= se.targetTouches.length
150
+
151
+ // Adjust for container's offset
152
+ // tx -= containerRect.left
153
+ // ty -= containerRect.top
154
+
155
+ //zoom at this average position
156
+ this.zoom(zoomFactor, this.pixToGeoX(tx), this.pixToGeoY(ty))
157
+ }
158
+ }
144
159
  }
145
160
 
146
161
  //center extent
@@ -215,6 +230,11 @@ export class GeoCanvas {
215
230
  throw new Error('Method redraw not implemented.')
216
231
  }
217
232
 
233
+ /** When the zoom level changes, ensures that any ongoing requests are aborted before new ones are initiated. */
234
+ cancelCurrentRequests() {
235
+ throw new Error('Method cancelCurrentRequests not implemented.')
236
+ }
237
+
218
238
  /**
219
239
  * Clear. To be used before a redraw for example.
220
240
  * @param {string} color
@@ -310,6 +330,7 @@ export class GeoCanvas {
310
330
  this.canvasSave.f /= f
311
331
  this.canvasSave.dx = this.geoToPixX(xGeo) * (1 - this.canvasSave.f)
312
332
  this.canvasSave.dy = this.geoToPixY(yGeo) * (1 - this.canvasSave.f)
333
+ this.clear(this.backgroundColor)
313
334
  this.offscreenCtx.drawImage(
314
335
  this.canvasSave.c,
315
336
  this.canvasSave.dx,
package/src/core/Map.js CHANGED
@@ -58,11 +58,25 @@ export class Map {
58
58
  this.geoCanvas.redraw = () => {
59
59
  this.redraw()
60
60
  }
61
+ this.geoCanvas.cancelCurrentRequests = () => {
62
+ // when the zoom level changes, avoid drawing outdated tiles, and ensure that requests are properly aborted when necessary
63
+ for (const layer of this.layers) {
64
+ //multires
65
+ if (layer.dataset?.datasets) {
66
+ for (const dataset of layer.dataset?.datasets) {
67
+ if (dataset?.cancelCurrentRequests) dataset.cancelCurrentRequests()
68
+ }
69
+ }
70
+ //single res
71
+ if (layer.dataset?.cancelCurrentRequests) layer.dataset?.cancelCurrentRequests()
72
+ }
73
+ }
61
74
 
62
75
  // legend div
63
- this.legendDivId = opts.legendDivId || 'gvizLegend'
64
- this.legend = select('#' + this.legendDivId)
65
- if (this.legend.empty()) this.initialiseLegend()
76
+ this.legend = opts.legendContainer
77
+ ? select(opts.legendContainer) // Wrap the provided HTML node in a D3 selection
78
+ : null
79
+ if (!this.legend) this.initialiseLegend()
66
80
 
67
81
  //tooltip
68
82
 
@@ -122,9 +136,9 @@ export class Map {
122
136
  }
123
137
 
124
138
  initialiseLegend() {
125
- this.legend = select(this.container.id && this.container.id != '' ? '#' + this.container.id : 'body')
126
- .append('div')
127
- .attr('id', this.legendDivId)
139
+ this.legend = select(this.container)
140
+ .append('div') // Create a new container
141
+ .attr('id', 'gridviz-legend')
128
142
  .style('position', 'absolute')
129
143
  .style('width', 'auto')
130
144
  .style('height', 'auto')
@@ -432,35 +446,43 @@ export class Map {
432
446
  * @memberof App
433
447
  */
434
448
  defineResizeObserver() {
435
- // listen to resize events
449
+ // Track whether the observer is currently processing a resize event
450
+ let resizePending = false
451
+
436
452
  const resizeObserver = new ResizeObserver((entries) => {
453
+ if (!Array.isArray(entries) || !entries.length) return
454
+
437
455
  let container = this.container
438
- // make sure canvas has been built
456
+
457
+ // Ensure the container has valid dimensions
439
458
  if (container.clientWidth > 0 && container.clientHeight > 0) {
440
- // make sure we dont exceed loop limit first
441
- // see: https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded
442
- window.requestAnimationFrame(() => {
443
- if (!Array.isArray(entries) || !entries.length) {
444
- return
445
- }
446
- // update the map and canvas size
447
- if (this.h !== container.clientHeight || this.w !== container.clientWidth) {
448
- this.h = container.clientHeight
449
- this.w = container.clientWidth
450
- this.geoCanvas.h = container.clientHeight
451
- this.geoCanvas.w = container.clientWidth
452
- this.geoCanvas.canvas.setAttribute('width', '' + this.w)
453
- this.geoCanvas.canvas.setAttribute('height', '' + this.h)
454
- // offscreen canvas
455
- this.geoCanvas.offscreenCanvas.setAttribute('width', '' + this.w)
456
- this.geoCanvas.offscreenCanvas.setAttribute('height', '' + this.h)
457
- this.redraw()
458
-
459
- //update button positions
460
- // if (this.zoomButtons) this.zoomButtons.node.style.left = this.w - 50 + 'px'
461
- // if (this.fullscreenButton) this.fullscreenButton.node.style.left = this.w - 50 + 'px'
462
- }
463
- })
459
+ if (!resizePending) {
460
+ resizePending = true // Prevent overlapping resize triggers
461
+
462
+ window.requestAnimationFrame(() => {
463
+ resizePending = false // Reset the flag after processing
464
+
465
+ // Check for size changes
466
+ if (this.h !== container.clientHeight || this.w !== container.clientWidth) {
467
+ this.h = container.clientHeight
468
+ this.w = container.clientWidth
469
+
470
+ // Update geoCanvas sizes
471
+ this.geoCanvas.h = this.h
472
+ this.geoCanvas.w = this.w
473
+ this.geoCanvas.canvas.setAttribute('width', String(this.w))
474
+ this.geoCanvas.canvas.setAttribute('height', String(this.h))
475
+ this.geoCanvas.offscreenCanvas.setAttribute('width', String(this.w))
476
+ this.geoCanvas.offscreenCanvas.setAttribute('height', String(this.h))
477
+
478
+ this.redraw()
479
+
480
+ // Optionally reposition UI elements
481
+ // if (this.zoomButtons) this.zoomButtons.node.style.left = this.w - 50 + 'px';
482
+ // if (this.fullscreenButton) this.fullscreenButton.node.style.left = this.w - 50 + 'px';
483
+ }
484
+ })
485
+ }
464
486
  }
465
487
  })
466
488
 
@@ -16,8 +16,6 @@ export class Tooltip {
16
16
  constructor(opts) {
17
17
  opts = opts || {}
18
18
 
19
- /** @type {string} */
20
- this.div = opts.div || 'tooltip_eurostat'
21
19
  /** @type {string} */
22
20
  this.maxWidth = opts.maxWidth || '20em'
23
21
  /** @type {string} */
@@ -47,21 +45,15 @@ export class Tooltip {
47
45
  this.xMouseOffset = opts.xMouseOffset || 0
48
46
  /** @type {HTMLElement} */
49
47
  this.parentElement = opts.parentElement || document.body
48
+ /** @type {HTMLElement} */
49
+ this.tooltipElement = opts.tooltipElement || null
50
50
 
51
51
  /**
52
52
  * @public
53
53
  * @type {import("d3-selection").Selection} */
54
- this.tooltip = select('#' + this.div)
55
-
56
- if (this.tooltip.empty()) {
57
- //create tooltip DOM node
58
- // this.tooltip = select(
59
- // '#' + this.parentElement.id && this.parentElement.id != ''
60
- // ? '#' + this.parentElement.id
61
- // : 'body'
62
- // )
63
- this.tooltip = select('body').append('div').attr('id', this.div)
64
- }
54
+ this.tooltip = opts.tooltipElement
55
+ ? select(opts.tooltipElement) // Wrap the provided HTML node in a D3 selection
56
+ : select(this.parentElement).append('div').attr('id', 'gridviz-tooltip').attr('class', 'gridviz-tooltip') // create default element
65
57
 
66
58
  //initialise
67
59
  this.tooltip.style('max-width', this.maxWidth)
@@ -77,6 +69,11 @@ export class Tooltip {
77
69
  this.tooltip.style('pointer-events', 'none')
78
70
  this.tooltip.style('opacity', '0')
79
71
  this.tooltip.style('text-wrap', 'nowrap')
72
+ this.tooltip.style('z-index', 99999999) // important for leaflet-gridviz etc
73
+
74
+ // these placeholders are needed to prevent an infinite DOM resizeObserver loop:
75
+ this.tooltip.style('left', '0')
76
+ this.tooltip.style('top', '0')
80
77
 
81
78
  // aria-labels (thanks to wahlatlas)
82
79
  this.tooltip.attr('role', 'tooltip').attr('aria-live', 'polite')
@@ -107,14 +104,51 @@ export class Tooltip {
107
104
  * @param {MouseEvent} event
108
105
  */
109
106
  setPosition(event) {
107
+ // Get the bounding rect of the parent container (map2)
110
108
  let parentRect = this.parentElement.getBoundingClientRect()
111
109
 
112
- let x = event.pageX + this.xOffset
113
- let y = event.pageY - this.yOffset
110
+ // Get the mouse position (relative to the parent container)
111
+ let x = event.clientX - parentRect.left + this.xOffset // Relative to parent
112
+ let y = event.clientY - parentRect.top - this.yOffset // Relative to parent
114
113
 
114
+ // Now, apply the position to the tooltip
115
115
  this.tooltip.style('left', x + 'px').style('top', y + 'px')
116
116
 
117
- this.ensureTooltipInsideContainer(event, parentRect)
117
+ // Ensure the tooltip stays inside the parent container
118
+ this.ensureTooltipInsideContainer(event, parentRect, this.tooltip.node())
119
+ }
120
+ /**
121
+ * @function ensureTooltipInsideContainer
122
+ * @description Prevents the tooltip from overflowing out of the App container (ensures that the tooltip is inside the gridviz container)
123
+ * @param {MouseEvent} event
124
+ * @param {DOMRect} parentRect
125
+ * @param {HTMLElement} tooltipNode
126
+ */
127
+ ensureTooltipInsideContainer(event, parentRect, tooltipNode) {
128
+ let node = tooltipNode
129
+ let parentWidth = parentRect.width
130
+ let parentHeight = parentRect.height
131
+
132
+ // Ensure tooltip doesn't go beyond the right edge
133
+ if (node.offsetLeft + node.clientWidth > parentWidth) {
134
+ let left = event.clientX - node.clientWidth - this.xOffset
135
+ node.style.left = left + 'px'
136
+ }
137
+
138
+ // Ensure tooltip doesn't go beyond the bottom edge
139
+ if (node.offsetTop + node.clientHeight > parentHeight) {
140
+ node.style.top = parentHeight - node.clientHeight + 'px'
141
+ }
142
+
143
+ // Ensure tooltip doesn't go above the top edge
144
+ if (node.offsetTop < 0) {
145
+ node.style.top = 0 + 'px'
146
+ }
147
+
148
+ // Ensure tooltip doesn't go beyond the left edge
149
+ if (node.offsetLeft < 0) {
150
+ node.style.left = 0 + 'px'
151
+ }
118
152
  }
119
153
 
120
154
  /*
@@ -145,39 +179,4 @@ export class Tooltip {
145
179
  this.tooltip.attr(k, v)
146
180
  return this
147
181
  }
148
-
149
- /**
150
- * @function ensureTooltipInsideContainer
151
- * @description Prevents the tooltip from overflowing out of the App container (ensures that the tooltip is inside the gridviz container)
152
- * @param {MouseEvent} event
153
- * @param {DOMRect} parentRect
154
- */
155
- ensureTooltipInsideContainer = function (event, parentRect) {
156
- let node = this.tooltip.node()
157
- let parentWidth = parentRect.width
158
- let parentHeight = parentRect.height
159
-
160
- //too far right
161
- if (node.offsetLeft > parentRect.left + parentWidth - node.clientWidth) {
162
- let left = event.x - node.clientWidth - this.xOffset
163
- node.style.left = left + 'px'
164
- // check if mouse covers tooltip
165
- if (node.offsetLeft + node.clientWidth > event.x) {
166
- //move tooltip left so it doesnt cover mouse
167
- let left2 = event.x - node.clientWidth - this.xOffset
168
- node.style.left = left2 + 'px'
169
- }
170
- // node.style.top = node.offsetTop + config.yOffset + "px";
171
- }
172
-
173
- //too far down
174
- if (node.offsetTop + node.clientHeight > parentRect.top + parentHeight) {
175
- node.style.top = node.offsetTop - node.clientHeight + 'px'
176
- }
177
-
178
- //too far up
179
- if (node.offsetTop < parentRect.top) {
180
- node.style.top = parentRect.top + this.yOffset + 'px'
181
- }
182
- }
183
182
  }
@@ -67,8 +67,7 @@ export class TiledGrid extends Dataset {
67
67
  this.infoLoadingStatus = 'failed'
68
68
  }
69
69
  })()
70
- } else if (this.infoLoadingStatus === 'loaded' || this.infoLoadingStatus === 'failed')
71
- this.map.redraw()
70
+ } else if (this.infoLoadingStatus === 'loaded' || this.infoLoadingStatus === 'failed') this.map.redraw()
72
71
  return this
73
72
  }
74
73
 
@@ -106,6 +105,10 @@ export class TiledGrid extends Dataset {
106
105
  async getData(extGeo) {
107
106
  if (!this.info) return this
108
107
 
108
+ // Create an AbortController for the current data request
109
+ this.abortController = new AbortController()
110
+ const signal = this.abortController.signal
111
+
109
112
  // Get the tiling envelope and check bounds
110
113
  const tb = this.getTilingEnvelope(extGeo)
111
114
  if (!tb) return this
@@ -117,10 +120,8 @@ export class TiledGrid extends Dataset {
117
120
  const yMin = Math.max(tb.yMin, gbYMin)
118
121
  const yMax = Math.min(tb.yMax, gbYMax)
119
122
 
120
- // Count the total number of tiles
121
123
  const totalTiles = (xMax - xMin + 1) * (yMax - yMin + 1)
122
-
123
- let processedTiles = 0 // Counter for loaded tiles
124
+ let processedTiles = 0
124
125
  const tilePromises = []
125
126
 
126
127
  // Iterate over tiles within bounds
@@ -128,8 +129,8 @@ export class TiledGrid extends Dataset {
128
129
  for (let yT = yMin; yT <= yMax; yT++) {
129
130
  if (!this.cache[xT]) this.cache[xT] = {}
130
131
 
131
- // Skip already loaded or loading tiles
132
- if (this.cache[xT][yT]) {
132
+ // Skip already loaded tiles or retry failed ones
133
+ if (this.cache[xT][yT] && this.cache[xT][yT] !== 'failed') {
133
134
  ++processedTiles
134
135
  continue
135
136
  }
@@ -137,21 +138,16 @@ export class TiledGrid extends Dataset {
137
138
  // Mark tile as loading
138
139
  this.cache[xT][yT] = 'loading'
139
140
 
140
- // Prepare the tile request and store it in tilePromises
141
141
  tilePromises.push(
142
- this.loadTile(xT, yT)
142
+ this.loadTile(xT, yT, signal)
143
143
  .then((tile) => {
144
- // Before we increment processedTiles, check if it's the last tile
145
- const isLastTile = ++processedTiles === totalTiles
146
-
147
- // Store the tile in the cache
148
144
  this.cache[xT][yT] = tile
149
145
 
150
- // Pass the isLastTile argument to checkAndRedraw
146
+ // Check if this is the last tile
147
+ const isLastTile = ++processedTiles === totalTiles
151
148
  this.checkAndRedraw(tile, isLastTile)
152
149
  })
153
150
  .catch(() => {
154
- // Mark as failed but allow processing to continue
155
151
  this.cache[xT][yT] = 'failed'
156
152
  ++processedTiles
157
153
  })
@@ -159,30 +155,47 @@ export class TiledGrid extends Dataset {
159
155
  }
160
156
  }
161
157
 
162
- // Handle all tile promises, including failures
163
158
  await Promise.allSettled(tilePromises)
164
159
  return this
165
160
  }
166
161
 
167
- async loadTile(xT, yT) {
162
+ /**
163
+ * Load a tile.
164
+ *
165
+ * @param {number} xT
166
+ * @param {number} yT
167
+ * @param {AbortSignal} signal
168
+ * @returns {Promise<any>}
169
+ */
170
+ async loadTile(xT, yT, signal) {
168
171
  try {
169
- const data = await csv(`${this.url}${xT}/${yT}.csv`)
172
+ const data = await csv(`${this.url}${xT}/${yT}.csv`, { signal })
170
173
 
171
- // Preprocess or filter the data if needed
172
174
  const cells = this.preprocess ? data.filter((cell) => this.preprocess(cell) !== false) : data
173
175
 
174
176
  if (!this.info) throw new Error('Tile info unknown')
175
177
 
176
178
  return getGridTile(cells, xT, yT, this.info)
177
179
  } catch (error) {
178
- console.warn(`Failed to load tile ${xT}, ${yT}:`, error)
180
+ if (error.name === 'AbortError') {
181
+ console.warn(`Tile request for ${xT}, ${yT} was aborted.`)
182
+ }
179
183
  throw error
180
184
  }
181
185
  }
182
186
 
187
+ /**
188
+ * Cancel ongoing data requests when zoom level changes.
189
+ */
190
+ cancelCurrentRequests() {
191
+ if (this.abortController) {
192
+ this.abortController.abort()
193
+ }
194
+ }
195
+
183
196
  checkAndRedraw(tile, isLastTile) {
184
197
  // Check if any visible layer depends on this dataset
185
- //check if redraw is really needed, that is if:
198
+ // check if redraw is really needed, that is if:
186
199
  // 1. the dataset belongs to a layer which is visible at the current zoom level
187
200
  let needsRedraw = false
188
201
  //go through the layers
@@ -15,7 +15,7 @@ export const ternaryClassifier = (properties, totalFunction, opts = {}) => {
15
15
  //parameter to decide wether to use mixed classes m0, m1, m2.
16
16
  const withMixedClasses = opts.withMixedClasses != undefined ? opts.withMixedClasses : true
17
17
 
18
- //paramerter decide wether to use a central class, and the size of this central class.
18
+ //parameter to decide wether to use a central class, and the size of this central class.
19
19
  //set to 0 or undefined for not showing any central class. Set to 1 for a central class that contains the mix classes
20
20
  const cc = opts.centerCoefficient ? 1 - opts.centerCoefficient : undefined
21
21
 
@@ -130,6 +130,11 @@ export const ternaryColorClassifier = (properties, totalFunction, colors, opts =
130
130
  return fun
131
131
  }
132
132
 
133
+
134
+
135
+
136
+ // OLD VERSIONS
137
+
133
138
  /*
134
139
  const orderedIndexesDec = arr => [...arr.keys()].sort((a, b) => arr[b] - arr[a]);
135
140
  const orderedIndexesInc = arr => [...arr.keys()].sort((a, b) => arr[a] - arr[b]);