gridviz 3.0.31 → 3.0.33

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.31",
3
+ "version": "3.0.33",
4
4
  "description": "Visualization tool for gridded statistics",
5
5
  "keywords": [
6
6
  "statistics",
@@ -30,7 +30,7 @@ export class Drawable {
30
30
  * The function parameter is the zoom level.
31
31
  * (see CanvasRenderingContext2D: globalCompositeOperation property)
32
32
  * @type {function(number):GlobalCompositeOperation} */
33
- this.blendOperation = opts.blendOperation || ((z) => 'source-over')
33
+ this.blendOperation = opts.blendOperation //|| ((z) => 'source-over')
34
34
 
35
35
  /** @type {(function(number):string)|undefined} */
36
36
  this.filterColor = opts.filterColor // (z) => "#eee7"
@@ -39,7 +39,7 @@ export class Drawable {
39
39
  }
40
40
 
41
41
  /**
42
- * Draw layer filter.
42
+ * Draw filter.
43
43
  *
44
44
  * @param {import("./GeoCanvas.js").GeoCanvas} geoCanvas The canvas where to draw the layer.
45
45
  * @returns {void}
package/src/core/Map.js CHANGED
@@ -76,6 +76,7 @@ export class Map {
76
76
  this.legend = opts.legendContainer
77
77
  ? select(opts.legendContainer) // Wrap the provided HTML node in a D3 selection
78
78
  : null
79
+ this._legendProvidedByUser = !!opts.legendContainer
79
80
  if (!this.legend) this.initialiseLegend()
80
81
 
81
82
  //tooltip
@@ -181,14 +182,19 @@ export class Map {
181
182
  const z = this.geoCanvas.view.z
182
183
  this.updateExtentGeo()
183
184
 
185
+ const ctx = this.geoCanvas.offscreenCtx
186
+
184
187
  //go through the layers
185
188
  for (const layer of this.layers) {
186
189
  //check if layer is visible
187
190
  if (layer.visible && !layer.visible(z)) continue
188
191
 
189
192
  //set layer alpha and blend mode
190
- this.geoCanvas.offscreenCtx.globalAlpha = layer.alpha ? layer.alpha(z) : 1.0
191
- if (layer.blendOperation) this.geoCanvas.offscreenCtx.globalCompositeOperation = layer.blendOperation(z)
193
+ if (layer.alpha || layer.blendOperation) {
194
+ ctx.save()
195
+ if (layer.alpha) ctx.globalAlpha = layer.alpha(z)
196
+ if (layer.blendOperation) ctx.globalCompositeOperation = layer.blendOperation(z)
197
+ }
192
198
 
193
199
  //set affin transform to draw with geographical coordinates
194
200
  this.geoCanvas.setCanvasTransform()
@@ -200,8 +206,7 @@ export class Map {
200
206
  if (layer.filterColor) layer.drawFilter(this.geoCanvas)
201
207
 
202
208
  //restore default alpha and blend operation
203
- this.geoCanvas.offscreenCtx.globalAlpha = 1.0
204
- this.geoCanvas.offscreenCtx.globalCompositeOperation = this.defaultGlobalCompositeOperation
209
+ if (layer.alpha || layer.blendOperation) ctx.restore()
205
210
  }
206
211
 
207
212
  // one drawImage call: draw the offscreen canvas to the main canvas
@@ -536,7 +541,10 @@ export class Map {
536
541
  this.geoCanvas.canvas.remove()
537
542
 
538
543
  // remove legend
539
- this.legend?.remove()
544
+ // only remove legend if gridviz created it
545
+ if (!this._legendProvidedByUser) {
546
+ this.legend?.remove()
547
+ }
540
548
 
541
549
  // remove tooltip
542
550
  this.tooltip.tooltip?.remove()
package/src/index.js CHANGED
@@ -23,6 +23,7 @@ export { SegmentStyle } from './style/SegmentStyle.js'
23
23
  export { TextStyle } from './style/TextStyle.js'
24
24
  export { PillarStyle } from './style/PillarStyle.js'
25
25
  export { SideStyle } from './style/SideStyle.js'
26
+ export { ShadingRayStyle } from './style/ShadingRayStyle.js'
26
27
  export { ShadingStyle } from './style/ShadingStyle.js'
27
28
  export { SideCategoryStyle } from './style/SideCategoryStyle.js'
28
29
  export { DotDensityStyle } from './style/DotDensityStyle.js'
@@ -35,6 +36,7 @@ export { NinjaStarStyle } from './style/NinjaStarStyle.js'
35
36
  export { TimeSeriesStyle } from './style/TimeSeriesStyle.js'
36
37
  export { IsoFenceStyle } from './style/IsoFenceStyle.js'
37
38
  export { ImageStyle } from './style/ImageStyle.js'
39
+ export { Interpolator } from './style/Interpolator.js'
38
40
 
39
41
  // export additional layers
40
42
  export { GridLayer } from './layer/GridLayer.js'
@@ -62,7 +64,7 @@ export { TernaryLegend } from './legend/TernaryLegend.js'
62
64
  export * from './utils/stretching.js'
63
65
  export * from './utils/scale.js'
64
66
  export * from './utils/ternary.js'
65
- export { nice } from './utils/utils.js'
67
+ export { nice, cellsToMatrix } from './utils/utils.js'
66
68
 
67
69
  import { GeoCanvas } from './core/GeoCanvas.js'
68
70
  export const getParameterByName = GeoCanvas.getParameterByName
@@ -60,15 +60,26 @@ export class GridLayer extends Layer {
60
60
  //update dataset view cache
61
61
  dsc.updateViewCache(geoCanvas.extGeo)
62
62
 
63
+
64
+ //set layer alpha and blend mode
65
+ if (this.alpha || this.blendOperation) {
66
+ ctx.save()
67
+ if (this.alpha) ctx.globalAlpha = this.alpha(z)
68
+ if (this.blendOperation) ctx.globalCompositeOperation = this.blendOperation(z)
69
+ }
70
+
63
71
  //draw cells, style by style
64
72
  for (const s of this.styles) {
65
73
  //check if style is visible
66
74
  if (s.visible && !s.visible(z)) continue
67
75
 
68
- //set style alpha and blend mode
69
- //TODO: multiply by layer alpha ?
70
- ctx.globalAlpha = s.alpha ? s.alpha(z) : 1.0
71
- if (s.blendOperation) ctx.globalCompositeOperation = s.blendOperation(z)
76
+ // set style alpha and blend mode
77
+ //TODO: multiply style alpha by layer alpha ?
78
+ if (s.alpha || s.blendOperation) {
79
+ ctx.save()
80
+ if (s.alpha) ctx.globalAlpha = s.alpha(z)
81
+ if (s.blendOperation) ctx.globalCompositeOperation = s.blendOperation(z)
82
+ }
72
83
 
73
84
  //set affin transform to draw with geographical coordinates
74
85
  geoCanvas.setCanvasTransform()
@@ -78,8 +89,14 @@ export class GridLayer extends Layer {
78
89
 
79
90
  //draw style filter
80
91
  if (s.filterColor) s.drawFilter(geoCanvas)
92
+
93
+ //restore default alpha and blend operation - before style change
94
+ if (s.alpha || s.blendOperation) ctx.restore()
81
95
  }
82
96
 
97
+ //restore default alpha and blend operation - before layer change
98
+ if (this.alpha || this.blendOperation) ctx.restore()
99
+
83
100
  //add legend element
84
101
  if (legend) {
85
102
  for (const s of this.styles) {
@@ -19,7 +19,7 @@ export class TernaryLegend extends Legend {
19
19
  this.classifier = opts.classifier
20
20
 
21
21
  this.width = opts.width || 150
22
- this.selectionColor = this.selectionColor || 'red'
22
+ this.selectionColor = opts.selectionColor || 'red'
23
23
  this.tooltip = opts.tooltip
24
24
  this.texts = opts.texts
25
25
 
@@ -0,0 +1,258 @@
1
+ //@ts-check
2
+ 'use strict'
3
+
4
+ import { Style } from '../core/Style.js'
5
+ import { cellsToMatrix } from '../utils/utils.js'
6
+
7
+ /**
8
+ * @module style
9
+ * @author Julien Gaffuri
10
+ */
11
+ export class Interpolator extends Style {
12
+ /** @param {object} opts */
13
+ constructor(opts) {
14
+ super(opts)
15
+ opts = opts || {}
16
+
17
+ /** The cell value to interpolate
18
+ * @type { function } } */
19
+ this.value = opts.value //(c) => elevation
20
+
21
+ /** The target resolution. As a function (resolution, z) => targetResolution
22
+ * NB: Try to make sure that resolution/targetResolution is an integer.
23
+ * @type { function } } */
24
+ this.targetResolution = opts.targetResolution || ((r, z) => r / 5)
25
+
26
+ /** the property name to store the interpolated value in the cell
27
+ * @type { string } } */
28
+ this.interpolatedProperty = opts.interpolatedProperty || 'value'
29
+
30
+ // the interpolation method: currently only 'bilinear' is supported
31
+ //this.method = opts.method || 'bilinear' // 'nearest', 'bilinear'
32
+
33
+ /** when a neighbor cell has no value, take zero value or average of surrounding cells
34
+ * @type { string } } */
35
+ this.nodataHandling = opts.nodataHandling || "zero" // 'zero', 'average'
36
+
37
+ /** The styles to represent the interpolated grid.
38
+ * @type {Array.<Style>} */
39
+ this.styles = opts.styles || []
40
+ }
41
+
42
+
43
+ /**
44
+ *
45
+ * @param {Array.<import("../core/Dataset.js").Cell>} cells
46
+ * @param {import("../core/GeoCanvas.js").GeoCanvas} geoCanvas
47
+ * @param {number} resolution
48
+ */
49
+ draw(cells, geoCanvas, resolution) {
50
+
51
+ //filter cells
52
+ if (this.filter) cells = cells.filter(this.filter)
53
+ if (cells.length == 0) return;
54
+
55
+ //
56
+ const z = geoCanvas.view.z
57
+
58
+ //get view scale
59
+ //const viewScale = this.viewScale ? this.viewScale(cells, resolution, z) : undefined
60
+
61
+ //index cells by y and x
62
+ let m = cellsToMatrix(cells, resolution, c => this.value(c))
63
+ const x0 = m.x0, y0 = m.y0
64
+
65
+ //get target resolution and scale factor
66
+ let targetResolution = this.targetResolution(resolution, z)
67
+ const scaleFactor = Math.round(resolution / targetResolution)
68
+ targetResolution = resolution / scaleFactor
69
+
70
+ // compute ray casting shadow
71
+ m = bilinearInterpolator(m, scaleFactor, this.nodataHandling);
72
+
73
+ // make cells
74
+ cells = []
75
+ const ra = (resolution - targetResolution) / 2
76
+ for (let i = 0; i < m.length; i++) {
77
+ const row = m[i]
78
+ const y = y0 - i * targetResolution + ra
79
+ for (let j = 0; j < row.length; j++) {
80
+ const v = row[j]
81
+ if (!v) continue
82
+ const c = { x: x0 + j * targetResolution + ra, y: y }
83
+ c[this.interpolatedProperty] = v
84
+ cells.push(c)
85
+ }
86
+ }
87
+
88
+ //draw smoothed cells from styles
89
+ const ctx = geoCanvas.offscreenCtx
90
+ for (let s of this.styles) {
91
+
92
+ //check if style is visible
93
+ if (s.visible && !s.visible(z)) continue
94
+
95
+ //set style alpha and blend mode
96
+ //TODO: multiply by layer alpha ?
97
+ if (s.alpha || s.blendOperation) {
98
+ ctx.save()
99
+ if (s.alpha) ctx.globalAlpha = s.alpha(z)
100
+ if (s.blendOperation) ctx.globalCompositeOperation = s.blendOperation(z)
101
+ }
102
+
103
+ //set affin transform to draw with geographical coordinates
104
+ geoCanvas.setCanvasTransform()
105
+
106
+ //draw with style
107
+ s.draw(cells, geoCanvas, targetResolution)
108
+
109
+ //draw style filter
110
+ if (s.filterColor) s.drawFilter(geoCanvas)
111
+
112
+ //restore ctx
113
+ if (s.alpha || s.blendOperation) ctx.restore()
114
+ }
115
+
116
+ }
117
+ }
118
+
119
+
120
+ const interp = (topLeft, topRight, bottomLeft, bottomRight, xRatio, yRatio) => {
121
+ const top = topLeft + (topRight - topLeft) * xRatio;
122
+ const bottom = bottomLeft + (bottomRight - bottomLeft) * xRatio;
123
+ return top + (bottom - top) * yRatio;
124
+ }
125
+
126
+
127
+ function bilinearInterpolator(grid, scaleFactor = 5, nodataHandling = "zero") {
128
+
129
+ if (scaleFactor === 1) return grid;
130
+
131
+ // input grid dimensions
132
+ const rows = grid.length;
133
+ if (rows === 0) return [];
134
+ const cols = grid[0].length;
135
+
136
+ // output grid dimensions
137
+ const fineRows = rows * scaleFactor;
138
+ const fineCols = cols * scaleFactor;
139
+ const fineGrid = Array(fineRows).fill().map(() => Array(fineCols).fill(undefined));
140
+
141
+ // check if scale factor is even
142
+ const sfIsEven = scaleFactor % 2 === 0
143
+
144
+ //compute output grid values
145
+ for (let i = 0; i < fineRows; i++) {
146
+
147
+ const coarseI = Math.min(Math.floor(i / scaleFactor), rows - 2);
148
+ const yRatio = (i % scaleFactor) / scaleFactor;
149
+ const i_ = coarseI * scaleFactor + (scaleFactor / 2)
150
+
151
+ for (let j = 0; j < fineCols; j++) {
152
+ const coarseJ = Math.min(Math.floor(j / scaleFactor), cols - 2);
153
+ const xRatio = (j % scaleFactor) / scaleFactor;
154
+ const j_ = coarseJ * scaleFactor + (scaleFactor / 2)
155
+
156
+ // get four corner values
157
+ const topLeft = grid[coarseI][coarseJ];
158
+ const topRight = grid[coarseI][coarseJ + 1];
159
+ const bottomLeft = grid[coarseI + 1][coarseJ];
160
+ const bottomRight = grid[coarseI + 1][coarseJ + 1];
161
+
162
+
163
+ if (sfIsEven) {
164
+ if (topLeft === undefined && i <= i_ && j <= j_) continue
165
+ if (topRight === undefined && i <= i_ && j >= j_) continue
166
+ if (bottomLeft === undefined && i >= i_ && j <= j_) continue
167
+ if (bottomRight === undefined && i >= i_ && j >= j_) continue
168
+ } else {
169
+ if (topLeft === undefined && i < i_ && j < j_) continue
170
+ if (topRight === undefined && i < i_ && j > j_) continue
171
+ if (bottomLeft === undefined && i > i_ && j < j_) continue
172
+ if (bottomRight === undefined && i > i_ && j > j_) continue
173
+ }
174
+
175
+ if(nodataHandling === "zero") {
176
+ // replace undefined values by zero
177
+ fineGrid[i][j] = interp(topLeft || 0, topRight || 0, bottomLeft || 0, bottomRight || 0, xRatio, yRatio)
178
+ continue
179
+ }
180
+
181
+ // general case: bilinear interpolation
182
+ // If all four are defined, interpolate normally
183
+ if (topLeft !== undefined && topRight !== undefined && bottomLeft !== undefined && bottomRight !== undefined) {
184
+ fineGrid[i][j] = interp(topLeft, topRight, bottomLeft, bottomRight, xRatio, yRatio)
185
+ continue
186
+ }
187
+
188
+ // If only one is not defined, use average value of 2 adjacents and interpolate
189
+ if (topLeft === undefined && topRight !== undefined && bottomLeft !== undefined && bottomRight !== undefined) {
190
+ const v = topRight + bottomLeft
191
+ fineGrid[i][j] = interp(v / 2, topRight, bottomLeft, bottomRight, xRatio, yRatio)
192
+ continue
193
+ }
194
+ if (topLeft !== undefined && topRight === undefined && bottomLeft !== undefined && bottomRight !== undefined) {
195
+ const v = topLeft + bottomRight
196
+ fineGrid[i][j] = interp(topLeft, v / 2, bottomLeft, bottomRight, xRatio, yRatio)
197
+ continue
198
+ }
199
+ if (topLeft !== undefined && topRight !== undefined && bottomLeft === undefined && bottomRight !== undefined) {
200
+ const v = topLeft + bottomRight
201
+ fineGrid[i][j] = interp(topLeft, topRight, v / 2, bottomRight, xRatio, yRatio)
202
+ continue
203
+ }
204
+ if (topLeft !== undefined && topRight !== undefined && bottomLeft !== undefined && bottomRight === undefined) {
205
+ const v = topRight + bottomLeft
206
+ fineGrid[i][j] = interp(topLeft, topRight, bottomLeft, v / 2, xRatio, yRatio)
207
+ continue
208
+ }
209
+
210
+ // If only two diagonally opposite points are defined, interpolate along diagonal
211
+ if (topLeft !== undefined && bottomRight !== undefined) {
212
+ const t = (xRatio + yRatio) * Math.SQRT1_2
213
+ fineGrid[i][j] = topLeft * (1 - t) + bottomRight * t;
214
+ continue
215
+ }
216
+ if (topRight !== undefined && bottomLeft !== undefined) {
217
+ const t = (1 - xRatio + yRatio) * Math.SQRT1_2
218
+ fineGrid[i][j] = topRight * (1 - t) + bottomLeft * t;
219
+ continue
220
+ }
221
+
222
+ // If only two adjacent points are defined, interpolate between them
223
+ if (topLeft !== undefined && bottomLeft !== undefined) {
224
+ fineGrid[i][j] = bottomLeft * yRatio + topLeft * (1 - yRatio);
225
+ continue
226
+ }
227
+ if (topRight !== undefined && bottomRight !== undefined) {
228
+ fineGrid[i][j] = bottomRight * yRatio + topRight * (1 - yRatio);
229
+ continue
230
+ }
231
+ if (topLeft !== undefined && topRight !== undefined) {
232
+ fineGrid[i][j] = topRight * xRatio + topLeft * (1 - xRatio);
233
+ }
234
+ if (bottomLeft !== undefined && bottomRight !== undefined) {
235
+ fineGrid[i][j] = bottomRight * xRatio + bottomLeft * (1 - xRatio);
236
+ continue
237
+ }
238
+
239
+ // If only one point is defined, use that value
240
+ if (topLeft !== undefined) {
241
+ fineGrid[i][j] = topLeft;
242
+ continue
243
+ }
244
+ if (topRight !== undefined) {
245
+ fineGrid[i][j] = topRight;
246
+ continue
247
+ }
248
+ if (bottomLeft !== undefined) {
249
+ fineGrid[i][j] = bottomLeft;
250
+ continue
251
+ }
252
+ if (bottomRight !== undefined) {
253
+ fineGrid[i][j] = bottomRight;
254
+ }
255
+ }
256
+ }
257
+ return fineGrid;
258
+ }
@@ -2,6 +2,7 @@
2
2
  'use strict'
3
3
 
4
4
  import { Style } from '../core/Style.js'
5
+ import { cellsToGrid } from '../utils/utils.js'
5
6
 
6
7
  /**
7
8
  * @module style
@@ -47,15 +48,70 @@ export class JoyPlotStyle extends Style {
47
48
 
48
49
  //index cells by y and x
49
50
  /** @type {object} */
50
- const ind = {}
51
- for (const cell of cells) {
52
- let row = ind[cell.y]
53
- if (!row) {
54
- row = {}
55
- ind[cell.y] = row
51
+ const ind = cellsToGrid(cells, cell => this.height(cell, resolution, z, viewScale))
52
+
53
+
54
+ //make grid
55
+ /** @type {object} */
56
+ /*const grid = cellsToGrid2(cells, resolution, cell => this.height(cell, resolution, z, viewScale))
57
+ if (!grid.maxI || !grid.maxJ) return
58
+
59
+ //TODO
60
+ const ys = { min: grid.y0 + resolution * grid.minI, max: grid.y0 + resolution * grid.maxI }
61
+
62
+ //draw lines, row by row, stating from the top
63
+ ctx.lineJoin = 'round'
64
+ for (let i = grid.minI; i <= grid.maxI; i++) {
65
+
66
+ //Get row data
67
+ const row = grid[i]
68
+ if (!row) continue
69
+
70
+ // y of the row
71
+ const y = grid.y0 - i * resolution
72
+
73
+ //store the previous height
74
+ let hG_
75
+
76
+ for (let j = grid.minJ; j <= grid.maxJ; j++) {
77
+ //get column value
78
+ let hG = row[j]
79
+ if (!hG) hG = 0
80
+
81
+ // x of the cell
82
+ const x = grid.x0 + j * resolution
83
+
84
+ if (hG || hG_) {
85
+ //draw line only when at least one of both values is non-null
86
+ //TODO test bezierCurveTo
87
+ ctx.lineTo(x + resolution / 2, y + hG)
88
+ } else {
89
+ //else move the point
90
+ ctx.moveTo(x + resolution / 2, y)
91
+ }
92
+ //store the previous value
93
+ hG_ = hG
94
+ }
95
+ //last point
96
+ if (hG_) ctx.lineTo(grid.x0 + resolution * grid.maxJ + resolution / 2, y)
97
+
98
+ //draw fill
99
+ const fc = this.fillColor(y, ys, resolution, z)
100
+ if (fc && fc != 'none') {
101
+ ctx.fillStyle = fc
102
+ ctx.fill()
103
+ }
104
+
105
+ //draw line
106
+ const lc = this.lineColor(y, ys, resolution, z)
107
+ const lw = this.lineWidth(y, ys, resolution, z)
108
+ if (lc && lc != 'none' && lw > 0) {
109
+ ctx.strokeStyle = lc
110
+ ctx.lineWidth = lw
111
+ ctx.stroke()
56
112
  }
57
- row[cell.x] = this.height(cell, resolution, z, viewScale)
58
113
  }
114
+ */
59
115
 
60
116
  //compute extent
61
117
  const e = geoCanvas.extGeo
@@ -65,7 +121,6 @@ export class JoyPlotStyle extends Style {
65
121
  const yMin = Math.floor(e.yMin / resolution) * resolution
66
122
  const yMax = Math.floor(e.yMax / resolution) * resolution
67
123
 
68
- /** @type {{min:number, max:number}} */
69
124
  const ys = { min: yMin, max: yMax }
70
125
 
71
126
  //draw lines, row by row, stating from the top
@@ -82,13 +137,11 @@ export class JoyPlotStyle extends Style {
82
137
  ctx.moveTo(xMin - resolution / 2, y)
83
138
 
84
139
  //store the previous height
85
- /** @type {number|undefined} */
86
140
  let hG_
87
141
 
88
142
  //go through the line cells
89
143
  for (let x = xMin; x <= xMax; x += resolution) {
90
144
  //get column value
91
- /** @type {number} */
92
145
  let hG = row[x]
93
146
  if (!hG) hG = 0
94
147