gridviz 3.0.16 → 3.0.18

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.16",
3
+ "version": "3.0.18",
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
 
@@ -11,80 +11,80 @@ export class ZoomButtons extends Button {
11
11
  * @param {Object} opts
12
12
  */
13
13
  constructor(opts) {
14
- super(opts);
15
-
16
- this.onZoom = opts.onZoom; // custom user event handler
17
- this.delta = opts.delta || 0.2;
18
-
14
+ super(opts)
15
+
16
+ this.onZoom = opts.onZoom // custom user event handler
17
+ this.delta = opts.delta || 0.2
18
+
19
19
  // Create zoom in button
20
- this.zoomInBtn = document.createElement('a');
21
- this.zoomInBtn.id = 'zoom-in';
22
- this.zoomInBtn.className = 'gridviz-zoom-button';
23
- this.zoomInBtn.title = 'Zoom in';
24
- this.zoomInBtn.textContent = '+';
20
+ this.zoomInBtn = document.createElement('a')
21
+ this.zoomInBtn.id = 'zoom-in'
22
+ this.zoomInBtn.className = 'gridviz-zoom-button'
23
+ this.zoomInBtn.title = 'Zoom in'
24
+ this.zoomInBtn.textContent = '+'
25
25
  this.zoomInBtn.addEventListener('click', (e) => {
26
- this.zoomIn(e);
27
- });
26
+ this.zoomIn(e)
27
+ })
28
28
  this.zoomInBtn.addEventListener('mouseover', () => {
29
- this.zoomInBtn.style.backgroundColor = 'lightgrey';
30
- });
29
+ this.zoomInBtn.style.backgroundColor = 'lightgrey'
30
+ })
31
31
  this.zoomInBtn.addEventListener('mouseout', () => {
32
- this.zoomInBtn.style.backgroundColor = '#ffffff';
33
- });
34
-
32
+ this.zoomInBtn.style.backgroundColor = '#ffffff'
33
+ })
34
+
35
35
  // Create zoom out button
36
- this.zoomOutBtn = document.createElement('a');
37
- this.zoomOutBtn.id = 'zoom-out';
38
- this.zoomOutBtn.className = 'gridviz-zoom-button';
39
- this.zoomOutBtn.title = 'Zoom out';
40
- this.zoomOutBtn.textContent = '-';
36
+ this.zoomOutBtn = document.createElement('a')
37
+ this.zoomOutBtn.id = 'zoom-out'
38
+ this.zoomOutBtn.className = 'gridviz-zoom-button'
39
+ this.zoomOutBtn.title = 'Zoom out'
40
+ this.zoomOutBtn.textContent = '-'
41
41
  this.zoomOutBtn.addEventListener('click', (e) => {
42
- this.zoomOut(e);
43
- });
42
+ this.zoomOut(e)
43
+ })
44
44
  this.zoomOutBtn.addEventListener('mouseover', () => {
45
- this.zoomOutBtn.style.backgroundColor = 'lightgrey';
46
- });
45
+ this.zoomOutBtn.style.backgroundColor = 'lightgrey'
46
+ })
47
47
  this.zoomOutBtn.addEventListener('mouseout', () => {
48
- this.zoomOutBtn.style.backgroundColor = '#ffffff';
49
- });
50
-
48
+ this.zoomOutBtn.style.backgroundColor = '#ffffff'
49
+ })
50
+
51
51
  // Set common styles for buttons
52
- const buttons = [this.zoomInBtn, this.zoomOutBtn];
52
+ const buttons = [this.zoomInBtn, this.zoomOutBtn]
53
53
  buttons.forEach((btn, index) => {
54
- btn.style.alignItems = 'center';
55
- btn.style.justifyContent = 'center';
56
- btn.style.display = 'flex';
57
- btn.style.border = 'none';
58
- btn.style.color = 'black';
59
- btn.style.textAlign = 'center';
60
- btn.style.textDecoration = 'none';
61
- btn.style.padding = '4px';
62
- btn.style.fontSize = '20px';
63
- btn.style.fontWeight = 'bold';
64
- btn.style.userSelect = 'none';
65
- btn.style.backgroundColor = '#ffffff';
66
- if (index === 0) btn.style.borderBottom = '1px solid grey'; // Zoom in button only
67
- });
68
-
54
+ btn.style.alignItems = 'center'
55
+ btn.style.justifyContent = 'center'
56
+ btn.style.display = 'flex'
57
+ btn.style.border = 'none'
58
+ btn.style.color = 'black'
59
+ btn.style.textAlign = 'center'
60
+ btn.style.textDecoration = 'none'
61
+ btn.style.padding = '4px'
62
+ btn.style.fontSize = '24px'
63
+ btn.style.fontWeight = 'bold'
64
+ btn.style.userSelect = 'none'
65
+ btn.style.backgroundColor = '#ffffff'
66
+ if (index === 0) btn.style.borderBottom = '1px solid grey' // Zoom in button only
67
+ })
68
+
69
69
  // Unset parent class height and display for dual buttons
70
- this.style('height', 'unset');
71
- this.style('display', 'unset');
72
-
70
+ this.style('height', 'unset')
71
+ this.style('display', 'unset')
72
+
73
73
  // Set position
74
74
  if (opts.x) {
75
- this.style('left', opts.x + 'px');
75
+ this.style('left', opts.x + 'px')
76
76
  } else {
77
- this.style('right', '10px');
77
+ this.style('right', '10px')
78
78
  }
79
79
  if (opts.y) {
80
- this.style('top', opts.y + 'px');
80
+ this.style('top', opts.y + 'px')
81
81
  } else {
82
- this.style('top', '10px');
82
+ this.style('top', '10px')
83
83
  }
84
-
84
+
85
85
  // Append buttons to the container
86
- this.div.node().appendChild(this.zoomInBtn);
87
- this.div.node().appendChild(this.zoomOutBtn);
86
+ this.div.node().appendChild(this.zoomInBtn)
87
+ this.div.node().appendChild(this.zoomOutBtn)
88
88
  }
89
89
 
90
90
  /* Zoom in */
@@ -87,24 +87,29 @@ export class GeoCanvas {
87
87
  .on('zoom', (e) => {
88
88
  const t = e.transform
89
89
  const f = tP.k / t.k
90
- if (f == 1) {
91
- //pan
90
+
91
+ // Get container's bounding rect to adjust for offsets
92
+ const containerRect = this.canvas.getBoundingClientRect()
93
+
94
+ // Adjust for container's offset and zoom center
95
+ const offsetX = e.sourceEvent.offsetX - containerRect.left
96
+ const offsetY = e.sourceEvent.offsetY - containerRect.top
97
+
98
+ if (f === 1) {
99
+ // Pan logic
92
100
  const dx = tP.x - t.x
93
101
  const dy = tP.y - t.y
94
102
  this.pan(dx * this.view.z, -dy * this.view.z)
95
103
  } else {
96
104
  const se = e.sourceEvent
105
+
97
106
  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
- )
107
+ // Zoom at mouse position, adjusted by container offset
108
+ this.zoom(f, this.pixToGeoX(offsetX), this.pixToGeoY(offsetY))
104
109
  } else if (se instanceof TouchEvent) {
105
110
  if (!se.targetTouches.length) return
106
- //pinch zoom
107
- //compute average position of the touches
111
+
112
+ // Compute average position of the touches
108
113
  let tx = 0,
109
114
  ty = 0
110
115
  for (let tt of se.targetTouches) {
@@ -113,7 +118,12 @@ export class GeoCanvas {
113
118
  }
114
119
  tx /= se.targetTouches.length
115
120
  ty /= se.targetTouches.length
116
- //zoom at this average position
121
+
122
+ // Adjust for container's offset
123
+ tx -= containerRect.left
124
+ ty -= containerRect.top
125
+
126
+ // Zoom at the average touch position
117
127
  this.zoom(f, this.pixToGeoX(tx), this.pixToGeoY(ty))
118
128
  }
119
129
  }
package/src/core/Map.js CHANGED
@@ -60,9 +60,10 @@ export class Map {
60
60
  }
61
61
 
62
62
  // legend div
63
- this.legendDivId = opts.legendDivId || 'gvizLegend'
64
- this.legend = select('#' + this.legendDivId)
65
- if (this.legend.empty()) this.initialiseLegend()
63
+ this.legend = opts.legendContainer
64
+ ? select(opts.legendContainer) // Wrap the provided HTML node in a D3 selection
65
+ : null
66
+ if (!this.legend) this.initialiseLegend()
66
67
 
67
68
  //tooltip
68
69
 
@@ -122,9 +123,9 @@ export class Map {
122
123
  }
123
124
 
124
125
  initialiseLegend() {
125
- this.legend = select(this.container.id && this.container.id != '' ? '#' + this.container.id : 'body')
126
- .append('div')
127
- .attr('id', this.legendDivId)
126
+ this.legend = select(this.container)
127
+ .append('div') // Create a new container
128
+ .attr('id', 'gridviz-legend')
128
129
  .style('position', 'absolute')
129
130
  .style('width', 'auto')
130
131
  .style('height', 'auto')
@@ -174,8 +175,7 @@ export class Map {
174
175
 
175
176
  //set layer alpha and blend mode
176
177
  this.geoCanvas.offscreenCtx.globalAlpha = layer.alpha ? layer.alpha(z) : 1.0
177
- if (layer.blendOperation)
178
- this.geoCanvas.offscreenCtx.globalCompositeOperation = layer.blendOperation(z)
178
+ if (layer.blendOperation) this.geoCanvas.offscreenCtx.globalCompositeOperation = layer.blendOperation(z)
179
179
 
180
180
  //set affin transform to draw with geographical coordinates
181
181
  this.geoCanvas.setCanvasTransform()
@@ -290,8 +290,8 @@ export class Map {
290
290
  /** @type {import("./Layer.js").Layer} */
291
291
  const layer = this.layers[i]
292
292
  if (layer.visible && !layer.visible(z)) continue
293
+ if (layer.cellInfoHTML === 'none') continue // this is necessary in order to not show tooltips for layers 'on top' (e.g. population circles on top of squares)
293
294
  if (!layer.cellInfoHTML) continue
294
- //if (layer.cellInfoHTML === 'none') continue
295
295
  if (!layer.getDataset) continue
296
296
  const dsc = layer.getDataset(z)
297
297
  if (!dsc) continue
@@ -433,35 +433,43 @@ export class Map {
433
433
  * @memberof App
434
434
  */
435
435
  defineResizeObserver() {
436
- // listen to resize events
436
+ // Track whether the observer is currently processing a resize event
437
+ let resizePending = false
438
+
437
439
  const resizeObserver = new ResizeObserver((entries) => {
440
+ if (!Array.isArray(entries) || !entries.length) return
441
+
438
442
  let container = this.container
439
- // make sure canvas has been built
443
+
444
+ // Ensure the container has valid dimensions
440
445
  if (container.clientWidth > 0 && container.clientHeight > 0) {
441
- // make sure we dont exceed loop limit first
442
- // see: https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded
443
- window.requestAnimationFrame(() => {
444
- if (!Array.isArray(entries) || !entries.length) {
445
- return
446
- }
447
- // update the map and canvas size
448
- if (this.h !== container.clientHeight || this.w !== container.clientWidth) {
449
- this.h = container.clientHeight
450
- this.w = container.clientWidth
451
- this.geoCanvas.h = container.clientHeight
452
- this.geoCanvas.w = container.clientWidth
453
- this.geoCanvas.canvas.setAttribute('width', '' + this.w)
454
- this.geoCanvas.canvas.setAttribute('height', '' + this.h)
455
- // offscreen canvas
456
- this.geoCanvas.offscreenCanvas.setAttribute('width', '' + this.w)
457
- this.geoCanvas.offscreenCanvas.setAttribute('height', '' + this.h)
458
- this.redraw()
459
-
460
- //update button positions
461
- // if (this.zoomButtons) this.zoomButtons.node.style.left = this.w - 50 + 'px'
462
- // if (this.fullscreenButton) this.fullscreenButton.node.style.left = this.w - 50 + 'px'
463
- }
464
- })
446
+ if (!resizePending) {
447
+ resizePending = true // Prevent overlapping resize triggers
448
+
449
+ window.requestAnimationFrame(() => {
450
+ resizePending = false // Reset the flag after processing
451
+
452
+ // Check for size changes
453
+ if (this.h !== container.clientHeight || this.w !== container.clientWidth) {
454
+ this.h = container.clientHeight
455
+ this.w = container.clientWidth
456
+
457
+ // Update geoCanvas sizes
458
+ this.geoCanvas.h = this.h
459
+ this.geoCanvas.w = this.w
460
+ this.geoCanvas.canvas.setAttribute('width', String(this.w))
461
+ this.geoCanvas.canvas.setAttribute('height', String(this.h))
462
+ this.geoCanvas.offscreenCanvas.setAttribute('width', String(this.w))
463
+ this.geoCanvas.offscreenCanvas.setAttribute('height', String(this.h))
464
+
465
+ this.redraw()
466
+
467
+ // Optionally reposition UI elements
468
+ // if (this.zoomButtons) this.zoomButtons.node.style.left = this.w - 50 + 'px';
469
+ // if (this.fullscreenButton) this.fullscreenButton.node.style.left = this.w - 50 + 'px';
470
+ }
471
+ })
472
+ }
465
473
  }
466
474
  })
467
475
 
@@ -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)
@@ -78,6 +70,10 @@ export class Tooltip {
78
70
  this.tooltip.style('opacity', '0')
79
71
  this.tooltip.style('text-wrap', 'nowrap')
80
72
 
73
+ // these placeholders are needed to prevent an infinite DOM resizeObserver loop:
74
+ this.tooltip.style('left', '0')
75
+ this.tooltip.style('top', '0')
76
+
81
77
  // aria-labels (thanks to wahlatlas)
82
78
  this.tooltip.attr('role', 'tooltip').attr('aria-live', 'polite')
83
79
  }
@@ -107,14 +103,51 @@ export class Tooltip {
107
103
  * @param {MouseEvent} event
108
104
  */
109
105
  setPosition(event) {
106
+ // Get the bounding rect of the parent container (map2)
110
107
  let parentRect = this.parentElement.getBoundingClientRect()
111
108
 
112
- let x = event.pageX + this.xOffset
113
- let y = event.pageY - this.yOffset
109
+ // Get the mouse position (relative to the parent container)
110
+ let x = event.clientX - parentRect.left + this.xOffset // Relative to parent
111
+ let y = event.clientY - parentRect.top - this.yOffset // Relative to parent
114
112
 
113
+ // Now, apply the position to the tooltip
115
114
  this.tooltip.style('left', x + 'px').style('top', y + 'px')
116
115
 
117
- this.ensureTooltipInsideContainer(event, parentRect)
116
+ // Ensure the tooltip stays inside the parent container
117
+ this.ensureTooltipInsideContainer(event, parentRect, this.tooltip.node())
118
+ }
119
+ /**
120
+ * @function ensureTooltipInsideContainer
121
+ * @description Prevents the tooltip from overflowing out of the App container (ensures that the tooltip is inside the gridviz container)
122
+ * @param {MouseEvent} event
123
+ * @param {DOMRect} parentRect
124
+ * @param {HTMLElement} tooltipNode
125
+ */
126
+ ensureTooltipInsideContainer(event, parentRect, tooltipNode) {
127
+ let node = tooltipNode
128
+ let parentWidth = parentRect.width
129
+ let parentHeight = parentRect.height
130
+
131
+ // Ensure tooltip doesn't go beyond the right edge
132
+ if (node.offsetLeft + node.clientWidth > parentWidth) {
133
+ let left = event.clientX - node.clientWidth - this.xOffset
134
+ node.style.left = left + 'px'
135
+ }
136
+
137
+ // Ensure tooltip doesn't go beyond the bottom edge
138
+ if (node.offsetTop + node.clientHeight > parentHeight) {
139
+ node.style.top = parentHeight - node.clientHeight + 'px'
140
+ }
141
+
142
+ // Ensure tooltip doesn't go above the top edge
143
+ if (node.offsetTop < 0) {
144
+ node.style.top = 0 + 'px'
145
+ }
146
+
147
+ // Ensure tooltip doesn't go beyond the left edge
148
+ if (node.offsetLeft < 0) {
149
+ node.style.left = 0 + 'px'
150
+ }
118
151
  }
119
152
 
120
153
  /*
@@ -145,39 +178,4 @@ export class Tooltip {
145
178
  this.tooltip.attr(k, v)
146
179
  return this
147
180
  }
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
181
  }
@@ -7,7 +7,7 @@
7
7
  * @param {number} v the value
8
8
  * @param {Array.<number>} breaks the breaks
9
9
  * @returns The class id, from 0 to breaks.length
10
- * @deprecated use getClassifier instead
10
+ * @deprecated use getClassifier instead.
11
11
  */
12
12
  export function getClass(v, breaks) {
13
13
  if (!breaks) return