gridviz 2.0.28 → 2.0.29
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/dist/gridviz.js +34315 -0
- package/package.json +3 -2
- package/src/App.js +651 -0
- package/src/BackgroundLayer.js +182 -0
- package/src/BackgroundLayerWMS.js +131 -0
- package/src/Dataset.js +76 -0
- package/src/DatasetComponent.js +103 -0
- package/src/GeoCanvas.js +419 -0
- package/src/LabelLayer.js +190 -0
- package/src/Layer.js +92 -0
- package/src/Legend.js +86 -0
- package/src/LineLayer.js +150 -0
- package/src/Style.js +114 -0
- package/src/Tooltip.js +150 -0
- package/src/dataset/COGeo.js +24 -0
- package/src/dataset/CSVGrid.js +105 -0
- package/src/dataset/GeoTIFF.js +127 -0
- package/src/dataset/GridTile.js +41 -0
- package/src/dataset/ParquetGrid.js +123 -0
- package/src/dataset/TiledGrid.js +294 -0
- package/src/index.js +176 -0
- package/src/legend/ColorCategoryLegend.js +124 -0
- package/src/legend/ColorDiscreteLegend.js +105 -0
- package/src/legend/ColorLegend.js +125 -0
- package/src/legend/SegmentOrientationLegend.js +87 -0
- package/src/legend/SegmentWidthLegend.js +97 -0
- package/src/legend/SizeLegend.js +141 -0
- package/src/style/CompositionStyle.js +294 -0
- package/src/style/ContourStyle.js +54 -0
- package/src/style/DotDensityStyle.js +156 -0
- package/src/style/JoyPlotStyle.js +135 -0
- package/src/style/KernelSmoothingOldStyle.js +221 -0
- package/src/style/KernelSmoothingStyle.js +115 -0
- package/src/style/KernelSmoothingWGLStyle.js +130 -0
- package/src/style/LegoStyle.js +112 -0
- package/src/style/MosaicStyle.js +107 -0
- package/src/style/NinjaStarStyle.js +124 -0
- package/src/style/PillarStyle.js +230 -0
- package/src/style/SegmentStyle.js +142 -0
- package/src/style/ShapeColorSizeStyle.js +126 -0
- package/src/style/SideCatStyle.js +172 -0
- package/src/style/SideStyle.js +218 -0
- package/src/style/SquareColorCatWGLStyle.js +120 -0
- package/src/style/SquareColorWGLStyle.js +146 -0
- package/src/style/StrokeStyle.js +131 -0
- package/src/style/TanakaStyle.js +160 -0
- package/src/style/TextStyle.js +132 -0
- package/src/utils/Utils.js +35 -0
- package/src/utils/WebGLRectangleColoring.js +106 -0
- package/src/utils/WebGLSquareColoring.js +118 -0
- package/src/utils/WebGLSquareColoringAdvanced.js +230 -0
- package/src/utils/WebGLSquareColoringCatAdvanced.js +133 -0
- package/src/utils/WebGLSquareColoringKS.js +153 -0
- package/src/utils/stretching.js +131 -0
- package/src/utils/webGLUtils.js +71 -0
- package/src/utils/zoomUtils.js +142 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
import { Style } from '../Style'
|
|
5
|
+
import { randomNormal } from 'd3-random'
|
|
6
|
+
import { checkWebGLSupport, makeWebGLCanvas } from '../utils/webGLUtils'
|
|
7
|
+
import { WebGLSquareColoring } from '../utils/WebGLSquareColoring'
|
|
8
|
+
import { color } from 'd3-color'
|
|
9
|
+
import { monitor, monitorDuration } from '../utils/Utils'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
*
|
|
13
|
+
* @author Julien Gaffuri
|
|
14
|
+
*/
|
|
15
|
+
export class DotDensityStyle extends Style {
|
|
16
|
+
/** @param {object} opts */
|
|
17
|
+
constructor(opts) {
|
|
18
|
+
super(opts)
|
|
19
|
+
opts = opts || {}
|
|
20
|
+
|
|
21
|
+
/** The name of the column/attribute of the tabular data where to retrieve the variable for dot number.
|
|
22
|
+
* @type {string} */
|
|
23
|
+
this.nbCol = opts.nbCol
|
|
24
|
+
|
|
25
|
+
/** A function returning the number of dots for a cell value.
|
|
26
|
+
* @type {function(number,number,import("../Style").Stat,number):number} */
|
|
27
|
+
this.nb = opts.nb || ((v, r, s, zf) => (((0.3 * r * r) / (zf * zf)) * v) / s.max)
|
|
28
|
+
|
|
29
|
+
/** The color of the dots. Same color for all dots within a cell.
|
|
30
|
+
* @type {function(import("../Dataset").Cell):string} */
|
|
31
|
+
this.color = opts.color || (() => '#FF5733')
|
|
32
|
+
|
|
33
|
+
/** A function returning the size of the dots, in geo unit.
|
|
34
|
+
* @type {function(number,number):number} */
|
|
35
|
+
this.dotSize = opts.dotSize //|| ((r, zf) => ...
|
|
36
|
+
|
|
37
|
+
/** A function returning the sigma of the distribution from the resolution, in geo unit.
|
|
38
|
+
* @type {function(number,number):number} */
|
|
39
|
+
this.sigma = opts.sigma //|| ((r,zf) => ...
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Draw cells as text.
|
|
44
|
+
*
|
|
45
|
+
* @param {Array.<import("../Dataset").Cell>} cells
|
|
46
|
+
* @param {number} r
|
|
47
|
+
* @param {import("../GeoCanvas").GeoCanvas} cg
|
|
48
|
+
*/
|
|
49
|
+
draw(cells, r, cg) {
|
|
50
|
+
if (monitor) monitorDuration('*** DotDensityStyle draw')
|
|
51
|
+
|
|
52
|
+
//filter
|
|
53
|
+
if (this.filter) cells = cells.filter(this.filter)
|
|
54
|
+
|
|
55
|
+
//zoom factor
|
|
56
|
+
const zf = cg.getZf()
|
|
57
|
+
|
|
58
|
+
let stat
|
|
59
|
+
if (this.nbCol) stat = Style.getStatistics(cells, (c) => c[this.nbCol], true)
|
|
60
|
+
if (!stat) return
|
|
61
|
+
|
|
62
|
+
//size of the dots
|
|
63
|
+
const sGeo = this.dotSize ? this.dotSize(r, zf) : 2 * zf
|
|
64
|
+
|
|
65
|
+
//make random function
|
|
66
|
+
const sig = this.sigma ? this.sigma(r, zf) : r * 0.4
|
|
67
|
+
const rand = randomNormal(0, sig)
|
|
68
|
+
|
|
69
|
+
if (monitor) monitorDuration(' preparation')
|
|
70
|
+
|
|
71
|
+
if (checkWebGLSupport()) {
|
|
72
|
+
//create canvas and webgl renderer
|
|
73
|
+
const cvWGL = makeWebGLCanvas(cg.w + '', cg.h + '')
|
|
74
|
+
if (!cvWGL) {
|
|
75
|
+
console.error('No webGL')
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
//create webGL program
|
|
80
|
+
const prog = new WebGLSquareColoring(cvWGL.gl, sGeo / zf)
|
|
81
|
+
|
|
82
|
+
if (monitor) monitorDuration(' webgl creation')
|
|
83
|
+
|
|
84
|
+
const r2 = r / 2
|
|
85
|
+
|
|
86
|
+
let col, offset, nb, cx, cy, cc
|
|
87
|
+
for (let c of cells) {
|
|
88
|
+
//get color
|
|
89
|
+
col = this.color(c)
|
|
90
|
+
if (!col || col === 'none') continue
|
|
91
|
+
|
|
92
|
+
//get offset
|
|
93
|
+
offset = this.offset(c, r, zf)
|
|
94
|
+
|
|
95
|
+
//number of dots
|
|
96
|
+
nb = this.nb(c[this.nbCol], r, stat, zf)
|
|
97
|
+
|
|
98
|
+
//cell center
|
|
99
|
+
cx = c.x + offset.dx + r2
|
|
100
|
+
cy = c.y + offset.dy + r2
|
|
101
|
+
|
|
102
|
+
//convert color
|
|
103
|
+
cc = color(col)
|
|
104
|
+
if (!cc) return
|
|
105
|
+
|
|
106
|
+
//random points
|
|
107
|
+
for (let i = 0; i <= nb; i++)
|
|
108
|
+
prog.addPointData2(cx + rand(), cy + rand(), cc.r, cc.g, cc.b, cc.opacity)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (monitor) monitorDuration(' data preparation')
|
|
112
|
+
|
|
113
|
+
//draw
|
|
114
|
+
prog.draw(cg.getWebGLTransform())
|
|
115
|
+
|
|
116
|
+
if (monitor) monitorDuration(' webgl drawing')
|
|
117
|
+
|
|
118
|
+
//draw in canvas geo
|
|
119
|
+
cg.initCanvasTransform()
|
|
120
|
+
cg.ctx.drawImage(cvWGL.canvas, 0, 0)
|
|
121
|
+
|
|
122
|
+
if (monitor) monitorDuration(' canvas drawing')
|
|
123
|
+
} else {
|
|
124
|
+
//draw with HTML canvas
|
|
125
|
+
|
|
126
|
+
//draw in geo coordinates
|
|
127
|
+
cg.setCanvasTransform()
|
|
128
|
+
|
|
129
|
+
for (let c of cells) {
|
|
130
|
+
//get color
|
|
131
|
+
const col = this.color(c)
|
|
132
|
+
if (!col || col === 'none') continue
|
|
133
|
+
//set color
|
|
134
|
+
cg.ctx.fillStyle = col
|
|
135
|
+
|
|
136
|
+
//get offset
|
|
137
|
+
const offset = this.offset(c, r, zf)
|
|
138
|
+
|
|
139
|
+
//number of dots
|
|
140
|
+
const nb = this.nb(c[this.nbCol], r, stat, zf)
|
|
141
|
+
|
|
142
|
+
//draw random dots
|
|
143
|
+
const cx = c.x + offset.dx + r / 2,
|
|
144
|
+
cy = c.y + offset.dy + r / 2
|
|
145
|
+
for (let i = 0; i <= nb; i++) {
|
|
146
|
+
cg.ctx.fillRect(cx + rand(), cy + rand(), sGeo, sGeo)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
//update legends
|
|
152
|
+
this.updateLegends({ style: this, r: r, zf: zf })
|
|
153
|
+
|
|
154
|
+
if (monitor) monitorDuration('*** DotDensityStyle end draw')
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
import { Style } from '../Style'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
*
|
|
8
|
+
* @author Julien Gaffuri
|
|
9
|
+
*/
|
|
10
|
+
export class JoyPlotStyle extends Style {
|
|
11
|
+
/** @param {object} opts */
|
|
12
|
+
constructor(opts) {
|
|
13
|
+
super(opts)
|
|
14
|
+
opts = opts || {}
|
|
15
|
+
|
|
16
|
+
/** The cell column where to get the value to represent.
|
|
17
|
+
* @type {string} */
|
|
18
|
+
this.heightCol = opts.heightCol
|
|
19
|
+
|
|
20
|
+
/** A function returning the height of a cell.
|
|
21
|
+
* @type {function(number,number,import("../Style").Stat|undefined,number):number} */
|
|
22
|
+
this.height = opts.height || ((v) => Math.sqrt(v))
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @type {function(number,{min:number, max:number},number,number):string} */
|
|
26
|
+
this.lineColor = opts.lineColor || ((y, ys, r, zf) => '#BBB')
|
|
27
|
+
/**
|
|
28
|
+
* @type {function(number,{min:number, max:number},number,number):number} */
|
|
29
|
+
this.lineWidth = opts.lineWidth || ((y, ys, r, zf) => zf)
|
|
30
|
+
/**
|
|
31
|
+
* @type {function(number,{min:number, max:number},number,number):string} */
|
|
32
|
+
this.fillColor = opts.fillColor || ((y, ys, r, zf) => '#c08c5968')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Draw cells as squares depending on their value.
|
|
37
|
+
*
|
|
38
|
+
* @param {Array.<import("../Dataset").Cell>} cells
|
|
39
|
+
* @param {number} r
|
|
40
|
+
* @param {import("../GeoCanvas").GeoCanvas} cg
|
|
41
|
+
* */
|
|
42
|
+
draw(cells, r, cg) {
|
|
43
|
+
//filter
|
|
44
|
+
if (this.filter) cells = cells.filter(this.filter)
|
|
45
|
+
|
|
46
|
+
cg.ctx.lineJoin = 'round'
|
|
47
|
+
|
|
48
|
+
//zoom factor
|
|
49
|
+
const zf = cg.getZf()
|
|
50
|
+
|
|
51
|
+
//compute statistics
|
|
52
|
+
const stat = Style.getStatistics(cells, (c) => c[this.heightCol], true)
|
|
53
|
+
|
|
54
|
+
//index cells by y and x
|
|
55
|
+
/** @type {object} */
|
|
56
|
+
const ind = {}
|
|
57
|
+
for (const cell of cells) {
|
|
58
|
+
let row = ind[cell.y]
|
|
59
|
+
if (!row) {
|
|
60
|
+
row = {}
|
|
61
|
+
ind[cell.y] = row
|
|
62
|
+
}
|
|
63
|
+
row[cell.x] = this.height(cell[this.heightCol], r, stat, zf)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
//compute extent
|
|
67
|
+
const e = cg.extGeo
|
|
68
|
+
if (!e) return
|
|
69
|
+
const xMin = Math.floor(e.xMin / r) * r
|
|
70
|
+
const xMax = Math.floor(e.xMax / r) * r
|
|
71
|
+
const yMin = Math.floor(e.yMin / r) * r
|
|
72
|
+
const yMax = Math.floor(e.yMax / r) * r
|
|
73
|
+
|
|
74
|
+
/** @type {{min:number, max:number}} */
|
|
75
|
+
const ys = { min: yMin, max: yMax }
|
|
76
|
+
|
|
77
|
+
//draw in geo coordinates
|
|
78
|
+
cg.setCanvasTransform()
|
|
79
|
+
|
|
80
|
+
//draw lines, row by row, stating from the top
|
|
81
|
+
for (let y = yMax; y >= yMin; y -= r) {
|
|
82
|
+
//get row
|
|
83
|
+
const row = ind[y]
|
|
84
|
+
|
|
85
|
+
//no row
|
|
86
|
+
if (!row) continue
|
|
87
|
+
|
|
88
|
+
//place first point
|
|
89
|
+
cg.ctx.beginPath()
|
|
90
|
+
cg.ctx.moveTo(xMin - r / 2, y)
|
|
91
|
+
|
|
92
|
+
//store the previous height
|
|
93
|
+
/** @type {number|undefined} */
|
|
94
|
+
let hG_
|
|
95
|
+
|
|
96
|
+
//go through the line cells
|
|
97
|
+
for (let x = xMin; x <= xMax; x += r) {
|
|
98
|
+
//get column value
|
|
99
|
+
/** @type {number} */
|
|
100
|
+
let hG = row[x]
|
|
101
|
+
if (!hG) hG = 0
|
|
102
|
+
|
|
103
|
+
if (hG || hG_) {
|
|
104
|
+
//draw line only when at least one of both values is non-null
|
|
105
|
+
//TODO test bezierCurveTo
|
|
106
|
+
cg.ctx.lineTo(x + r / 2, y + hG)
|
|
107
|
+
} else {
|
|
108
|
+
//else move the point
|
|
109
|
+
cg.ctx.moveTo(x + r / 2, y)
|
|
110
|
+
}
|
|
111
|
+
//store the previous value
|
|
112
|
+
hG_ = hG
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
//last point
|
|
116
|
+
if (hG_) cg.ctx.lineTo(xMax + r / 2, y)
|
|
117
|
+
|
|
118
|
+
//draw fill
|
|
119
|
+
const fc = this.fillColor(y, ys, r, zf)
|
|
120
|
+
if (fc && fc != 'none') {
|
|
121
|
+
cg.ctx.fillStyle = fc
|
|
122
|
+
cg.ctx.fill()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
//draw line
|
|
126
|
+
const lc = this.lineColor(y, ys, r, zf)
|
|
127
|
+
const lw = this.lineWidth(y, ys, r, zf)
|
|
128
|
+
if (lc && lc != 'none' && lw > 0) {
|
|
129
|
+
cg.ctx.strokeStyle = lc
|
|
130
|
+
cg.ctx.lineWidth = lw
|
|
131
|
+
cg.ctx.stroke()
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
import { Style } from '../Style'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A style representing the cell as a smoothed layer, to smoothing local variations and show main trends across space.
|
|
8
|
+
*
|
|
9
|
+
* @author Julien Gaffuri
|
|
10
|
+
*/
|
|
11
|
+
export class KernelSmoothingStyle extends Style {
|
|
12
|
+
//bandwidth
|
|
13
|
+
// https://observablehq.com/@uwdata/fast-kde
|
|
14
|
+
// https://observablehq.com/d/3127b6d89ada959f
|
|
15
|
+
//TODO use https://github.com/uwdata/fast-kde ?
|
|
16
|
+
//TODO https://observablehq.com/@sahilchinoy/areal-interpolation-iii ?
|
|
17
|
+
|
|
18
|
+
/** @param {object} opts */
|
|
19
|
+
constructor(opts) {
|
|
20
|
+
super(opts)
|
|
21
|
+
opts = opts || {}
|
|
22
|
+
|
|
23
|
+
/** A function specifying the value to consider for each cell. This is the value to smooth.
|
|
24
|
+
* @type {function(import("../Dataset").Cell):number} */
|
|
25
|
+
this.value = opts.value
|
|
26
|
+
|
|
27
|
+
/** The smoothing parameter, in geo unit. The larger, the more smoothed.
|
|
28
|
+
* @type {function(number,number):number}
|
|
29
|
+
*/
|
|
30
|
+
this.sigma = opts.sigma // (r, zf)=>...
|
|
31
|
+
|
|
32
|
+
/** The styles to represent the smoothed cells.
|
|
33
|
+
* @type {Array.<Style>}
|
|
34
|
+
*/
|
|
35
|
+
this.styles = opts.styles
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Compute the kernel matrix, that is the matrix of the weights.
|
|
40
|
+
* One quadrant is necessary only, since it is symetrical (along both x and y axes).
|
|
41
|
+
* @param {number} s Sigma (in grid pixel !)
|
|
42
|
+
* @returns {Array.<Array<number>>}
|
|
43
|
+
* @private
|
|
44
|
+
*/
|
|
45
|
+
getKernelMatrix(s) {
|
|
46
|
+
//the size of the kernel: lets limit that to ~3 times the standard deviation, as an approximation.
|
|
47
|
+
const kernelSize = Math.floor(3.25 * s)
|
|
48
|
+
|
|
49
|
+
//pre-compute coefficient for gaussian computation, to avoid computing them every time.
|
|
50
|
+
const c2 = 2 * s * s
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The gaussian function. TODO expose that function as a parameter, to use other kernels ?
|
|
54
|
+
* @param {number} x
|
|
55
|
+
* @param {number} y
|
|
56
|
+
* @returns {number}
|
|
57
|
+
*/
|
|
58
|
+
const gaussian = (x, y) => (c2 != 0 ? Math.exp(-(x * x + y * y) / c2) : x == 0 && y == 0 ? 1 : 0) //dirac case
|
|
59
|
+
|
|
60
|
+
/** @type {Array.<Array<number>>} */
|
|
61
|
+
const kw = []
|
|
62
|
+
for (let wi = 0; wi <= kernelSize; wi++) {
|
|
63
|
+
/** @type {Array<number>} */
|
|
64
|
+
const col = []
|
|
65
|
+
for (let wj = 0; wj <= kernelSize; wj++) {
|
|
66
|
+
//compute weight at wi,wj
|
|
67
|
+
col.push(gaussian(wi, wj))
|
|
68
|
+
}
|
|
69
|
+
kw.push(col)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/*
|
|
73
|
+
//debug: show values
|
|
74
|
+
for (let wj = kernelSize-1; wj >=0; wj--) {
|
|
75
|
+
let st = "";
|
|
76
|
+
for (let wi = 0; wi <= kernelSize; wi++) {
|
|
77
|
+
st += " " + Math.floor(kw[wi][wj].w *1000) /1000
|
|
78
|
+
}
|
|
79
|
+
console.log(st)
|
|
80
|
+
}*/
|
|
81
|
+
|
|
82
|
+
return kw
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Compute kernel smoothing of some cells.
|
|
87
|
+
*
|
|
88
|
+
* @private
|
|
89
|
+
* @param {Array.<import("../Dataset").Cell>} cells The cells to be smoothed.
|
|
90
|
+
* @param {import("../Dataset").Envelope} e Geo envelope to consider.
|
|
91
|
+
* @param {number} r Resolution, in geo unit.
|
|
92
|
+
* @param {number} s Sigma (in grid pixel !)
|
|
93
|
+
* @returns {Array.<import("../Dataset").Cell>} The list of cells, including the initial ones and the ones with smoothed values, in "ksmval" property.
|
|
94
|
+
*/
|
|
95
|
+
kernelSmoothing(cells, e, r, s) {
|
|
96
|
+
//filter
|
|
97
|
+
if (this.filter) cells = cells.filter(this.filter)
|
|
98
|
+
|
|
99
|
+
const wThr = 0.01 //gaussian weights below this value will be ignored
|
|
100
|
+
|
|
101
|
+
//compute extent, in grid position
|
|
102
|
+
const xMin = Math.floor(e.xMin / r) * r
|
|
103
|
+
const xMax = Math.floor(e.xMax / r) * r
|
|
104
|
+
const yMin = Math.floor(e.yMin / r) * r
|
|
105
|
+
const yMax = Math.floor(e.yMax / r) * r
|
|
106
|
+
|
|
107
|
+
//compute matrix dimensions
|
|
108
|
+
const nbX = (xMax - xMin) / r + 1
|
|
109
|
+
const nbY = (yMax - yMin) / r + 1
|
|
110
|
+
|
|
111
|
+
//initialise smoothed value to 0
|
|
112
|
+
for (const c of cells) c['ksmval'] = 0
|
|
113
|
+
|
|
114
|
+
//make matrix. add input cells by i/j (grid position)
|
|
115
|
+
const index = {}
|
|
116
|
+
for (const c of cells) {
|
|
117
|
+
// i,j of the cell
|
|
118
|
+
const i = Math.floor((c.x - xMin) / r)
|
|
119
|
+
const j = Math.floor((c.y - yMin) / r)
|
|
120
|
+
if (!index[i]) index[i] = {}
|
|
121
|
+
index[i][j] = c
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Check that the cell i,j is within the frame */
|
|
125
|
+
const isWithinFrame = (i, j) => i >= 0 && i < nbX && j >= 0 && j < nbY
|
|
126
|
+
|
|
127
|
+
//get kernel matrix
|
|
128
|
+
/** @type {Array.<Array.<number>>} */
|
|
129
|
+
const kernelMatrix = this.getKernelMatrix(s)
|
|
130
|
+
const kernelSize = kernelMatrix.length
|
|
131
|
+
|
|
132
|
+
//compute summ of the weights over entire kernel window
|
|
133
|
+
/** @type {number} */
|
|
134
|
+
let sumWeights = 0
|
|
135
|
+
for (let ki = 1 - kernelSize; ki < kernelSize; ki++) {
|
|
136
|
+
for (let kj = 1 - kernelSize; kj < kernelSize; kj++) {
|
|
137
|
+
let w = kernelMatrix[Math.abs(ki)][Math.abs(kj)]
|
|
138
|
+
if (w < wThr) continue
|
|
139
|
+
sumWeights += w
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Add v as a contribution to the cell i,j */
|
|
144
|
+
const addContributionTo = (i, j, v) => {
|
|
145
|
+
//get cell at (i,j)
|
|
146
|
+
const c_ = index[i] ? index[i][j] : undefined
|
|
147
|
+
|
|
148
|
+
if (c_) {
|
|
149
|
+
//cell exists: add contribution
|
|
150
|
+
if (c_.ksmval) c_.ksmval += v
|
|
151
|
+
else c_.ksmval = v
|
|
152
|
+
} else {
|
|
153
|
+
//cell does not exist: create a new one with the smoothed value
|
|
154
|
+
if (!index[i]) index[i] = {}
|
|
155
|
+
index[i][j] = { x: xMin + i * r, y: yMin + j * r, ksmval: v }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
//compute smoothing, input cell by input cell
|
|
160
|
+
for (const c of cells) {
|
|
161
|
+
/** get value of cell c
|
|
162
|
+
* @type {number} */
|
|
163
|
+
const val = this.value(c)
|
|
164
|
+
if (!val) continue
|
|
165
|
+
|
|
166
|
+
//cell matrix coordinates
|
|
167
|
+
const i = Math.floor((c.x - xMin) / r)
|
|
168
|
+
const j = Math.floor((c.y - yMin) / r)
|
|
169
|
+
|
|
170
|
+
//add contributions to smoothed values
|
|
171
|
+
for (let ki = 1 - kernelSize; ki < kernelSize; ki++) {
|
|
172
|
+
for (let kj = 1 - kernelSize; kj < kernelSize; kj++) {
|
|
173
|
+
//check cell is within the frame
|
|
174
|
+
if (!isWithinFrame(i + ki, j + kj)) continue
|
|
175
|
+
|
|
176
|
+
//get contribution (ki,kj)
|
|
177
|
+
let w = kernelMatrix[Math.abs(ki)][Math.abs(kj)]
|
|
178
|
+
if (!w || w < wThr) continue
|
|
179
|
+
let v = w * val
|
|
180
|
+
if (!v) continue
|
|
181
|
+
v /= sumWeights
|
|
182
|
+
if (!v) continue
|
|
183
|
+
|
|
184
|
+
//add contribution
|
|
185
|
+
addContributionTo(i + ki, j + kj, v)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
//make output list
|
|
191
|
+
const out = []
|
|
192
|
+
for (let i of Object.keys(index)) for (const j of Object.keys(index[i])) out.push(index[i][j])
|
|
193
|
+
|
|
194
|
+
return out
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Draw the smoothed cells depending on the list of styles specified.
|
|
199
|
+
*
|
|
200
|
+
* @param {Array.<import("../Dataset").Cell>} cells
|
|
201
|
+
* @param {number} r
|
|
202
|
+
* @param {import("../GeoCanvas").GeoCanvas} cg
|
|
203
|
+
*/
|
|
204
|
+
draw(cells, r, cg) {
|
|
205
|
+
if (!cells || cells.length == 0) return
|
|
206
|
+
|
|
207
|
+
//get smoothing param in geo unit
|
|
208
|
+
/** @type {number} */
|
|
209
|
+
const sG = this.sigma(r, cg.zf)
|
|
210
|
+
|
|
211
|
+
//apply kernel smoothing
|
|
212
|
+
cells = this.kernelSmoothing(cells, cg.extGeo, r, sG / r)
|
|
213
|
+
|
|
214
|
+
//draw smoothed cells from styles
|
|
215
|
+
for (let s of this.styles) s.draw(cells, r, cg)
|
|
216
|
+
|
|
217
|
+
//update legends
|
|
218
|
+
//for (let s of this.styles)
|
|
219
|
+
// s.updateLegends({ style: s, r: r, zf: cg.getZf() });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
import { Style } from '../Style'
|
|
5
|
+
import { density2d } from 'fast-kde'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A style representing the cell as a smoothed layer, to smoothing local variations and show main trends across space.
|
|
9
|
+
*
|
|
10
|
+
* @author Julien Gaffuri
|
|
11
|
+
*/
|
|
12
|
+
export class KernelSmoothingStyle extends Style {
|
|
13
|
+
// https://observablehq.com/d/5dd1cb5e4d21c021
|
|
14
|
+
// https://observablehq.com/@uwdata/fast-kde
|
|
15
|
+
// https://observablehq.com/d/3127b6d89ada959f
|
|
16
|
+
//TODO https://observablehq.com/@sahilchinoy/areal-interpolation-iii ?
|
|
17
|
+
|
|
18
|
+
/** @param {object} opts */
|
|
19
|
+
constructor(opts) {
|
|
20
|
+
super(opts)
|
|
21
|
+
opts = opts || {}
|
|
22
|
+
|
|
23
|
+
/** A function returning the value to consider for each cell. This is the value to be smoothed.
|
|
24
|
+
* @type {function(import("../Dataset").Cell):number} */
|
|
25
|
+
this.value = opts.value
|
|
26
|
+
|
|
27
|
+
/** The smoothing parameter, in geo unit. The larger, the more smoothed.
|
|
28
|
+
* @type {function(number,number):number}
|
|
29
|
+
*/
|
|
30
|
+
this.sigma = opts.sigma // (r, zf)=>...
|
|
31
|
+
|
|
32
|
+
/** A factor to adjust the smoothed grid resolution.
|
|
33
|
+
* When set to 1, the smoothed grid is exactly the screen resolution.
|
|
34
|
+
* Set to 2 to degrade the resolution to a factor 2.
|
|
35
|
+
* The higher, the more pixelised and the faster to compute.
|
|
36
|
+
* @type { number }
|
|
37
|
+
*/
|
|
38
|
+
this.factor = opts.factor || 2
|
|
39
|
+
|
|
40
|
+
/** A filter function to filter the smoothed cells based on their smoothed value.
|
|
41
|
+
* Return true to keep the cell, false otherwise.
|
|
42
|
+
* @type { function(number):boolean }
|
|
43
|
+
*/
|
|
44
|
+
this.filterSm = opts.filterSm
|
|
45
|
+
|
|
46
|
+
/** The name of the attribute where the smoothed value is stored in the output smoothed grid.
|
|
47
|
+
* @type { string }
|
|
48
|
+
*/
|
|
49
|
+
this.sCol = opts.sCol || 'ksmval'
|
|
50
|
+
|
|
51
|
+
/** The styles to represent the smoothed grid.
|
|
52
|
+
* @type {Array.<Style>}
|
|
53
|
+
*/
|
|
54
|
+
this.styles = opts.styles || []
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Draw the smoothed cells depending on the list of styles specified.
|
|
59
|
+
*
|
|
60
|
+
* @param {Array.<import("../Dataset").Cell>} cells
|
|
61
|
+
* @param {number} r
|
|
62
|
+
* @param {import("../GeoCanvas").GeoCanvas} cg
|
|
63
|
+
*/
|
|
64
|
+
draw(cells, r, cg) {
|
|
65
|
+
//filter
|
|
66
|
+
if (this.filter) cells = cells.filter(this.filter)
|
|
67
|
+
|
|
68
|
+
if (!cells || cells.length == 0) return
|
|
69
|
+
|
|
70
|
+
//get smoothing param in geo unit
|
|
71
|
+
/** @type {number} */
|
|
72
|
+
const sG = this.sigma(r, cg.zf)
|
|
73
|
+
|
|
74
|
+
//compute smoothed grid dimensions
|
|
75
|
+
const nbX = Math.ceil(cg.w / this.factor)
|
|
76
|
+
const nbY = Math.ceil(cg.h / this.factor)
|
|
77
|
+
//compute smoothed grid geo extent
|
|
78
|
+
const e_ = [
|
|
79
|
+
[cg.pixToGeoX(0), cg.pixToGeoX(nbX * this.factor)],
|
|
80
|
+
[cg.pixToGeoY(nbY * this.factor), cg.pixToGeoY(0)],
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
//compute smoothed grid
|
|
84
|
+
let g = density2d(cells, {
|
|
85
|
+
x: (c) => c.x + r / 2,
|
|
86
|
+
y: (c) => c.y + r / 2,
|
|
87
|
+
weight: (c) => this.value(c),
|
|
88
|
+
bins: [nbX, nbY],
|
|
89
|
+
bandwidth: sG,
|
|
90
|
+
extent: e_,
|
|
91
|
+
}).grid()
|
|
92
|
+
|
|
93
|
+
//compute the resolution of the smoothed grid
|
|
94
|
+
const resSmoothed = (e_[0][1] - e_[0][0]) / nbX
|
|
95
|
+
|
|
96
|
+
//make smoothed cells
|
|
97
|
+
cells = []
|
|
98
|
+
for (let ind = 0; ind < g.length; ind++) {
|
|
99
|
+
const v = g[ind]
|
|
100
|
+
if (this.filterSm && !this.filterSm(v)) continue
|
|
101
|
+
const row = Math.floor(ind / nbX)
|
|
102
|
+
const col = ind % nbX
|
|
103
|
+
const c = { x: e_[0][0] + col * resSmoothed, y: e_[1][0] + row * resSmoothed }
|
|
104
|
+
c[this.sCol] = v
|
|
105
|
+
cells.push(c)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
//draw smoothed cells from styles
|
|
109
|
+
for (let s of this.styles) s.draw(cells, resSmoothed, cg)
|
|
110
|
+
|
|
111
|
+
//update legends
|
|
112
|
+
//for (let s of this.styles)
|
|
113
|
+
// s.updateLegends({ style: s, r: r, zf: cg.getZf() });
|
|
114
|
+
}
|
|
115
|
+
}
|