gridviz 2.1.3 → 2.1.4

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": "2.1.3",
3
+ "version": "2.1.4",
4
4
  "description": "Visualization tool for gridded statistics",
5
5
  "keywords": [
6
6
  "statistics",
package/src/App.js CHANGED
@@ -354,13 +354,14 @@ export class App {
354
354
  const layer = this.layers[i]
355
355
  if (!layer.visible) continue
356
356
  if (!layer.cellInfoHTML) continue
357
- if (layer.cellInfoHTML === 'none') continue
357
+ //if (layer.cellInfoHTML === 'none') continue
358
358
  const dsc = layer.getDatasetComponent(zf)
359
359
  if (!dsc) continue
360
360
 
361
361
  //get cell at mouse position
362
362
  /** @type {import('./Dataset').Cell|undefined} */
363
363
  const cell = dsc.getCellFromPosition(posGeo, dsc.getViewCache())
364
+ //console.log(cell, dsc.resolution)
364
365
  if (!cell) return undefined
365
366
  const html = layer.cellInfoHTML(cell, dsc.getResolution())
366
367
  if (!html) return undefined
package/src/Layer.js CHANGED
@@ -34,7 +34,7 @@ export class Layer {
34
34
  /** A function returning the blend operation. The function parameter is the zoom factor.
35
35
  * (see CanvasRenderingContext2D: globalCompositeOperation property)
36
36
  * @type {GlobalCompositeOperation} */
37
- this.blendOperation = opts.blendOperation || (zf => 'normal')
37
+ this.blendOperation = opts.blendOperation || (zf => "source-over")
38
38
 
39
39
  /** The minimum zoom factor: Below this level, the layer is not shown.
40
40
  * @type {number} */
package/src/Style.js CHANGED
@@ -46,7 +46,7 @@ export class Style {
46
46
  /** A function returning the blend operation. The function parameter is the zoom factor.
47
47
  * (see CanvasRenderingContext2D: globalCompositeOperation property)
48
48
  * @type {function(number):GlobalCompositeOperation} */
49
- this.blendOperation = opts.blendOperation || (zf => 'normal')
49
+ this.blendOperation = opts.blendOperation || (zf => "source-over")
50
50
 
51
51
  /** The minimum zoom factor: Below this level, the layer is not shown.
52
52
  * @type {number}
@@ -137,82 +137,82 @@ export class TiledGrid extends DatasetComponent {
137
137
  if (tile) continue
138
138
 
139
139
  //mark tile as loading
140
- this.cache[xT][yT] = 'loading'
141
- ; (async () => {
142
- //request tile
140
+ this.cache[xT][yT] = "loading";
141
+ (async () => {
142
+ //request tile
143
+ /** @type {Array.<import("../Dataset").Cell>} */
144
+ let cells
145
+
146
+ try {
143
147
  /** @type {Array.<import("../Dataset").Cell>} */
144
- let cells
145
-
146
- try {
147
- /** @type {Array.<import("../Dataset").Cell>} */
148
- // @ts-ignore
149
- const data = await csv(this.url + xT + '/' + yT + '.csv')
150
-
151
- if (monitor) monitorDuration('*** TiledGrid parse start')
152
-
153
- //preprocess/filter
154
- if (this.preprocess) {
155
- cells = []
156
- for (const c of data) {
157
- const b = this.preprocess(c)
158
- if (b == false) continue
159
- cells.push(c)
160
- }
161
- } else {
162
- cells = data
148
+ // @ts-ignore
149
+ const data = await csv(this.url + xT + '/' + yT + '.csv')
150
+
151
+ //if (monitor) monitorDuration('*** TiledGrid parse start')
152
+
153
+ //preprocess/filter
154
+ if (this.preprocess) {
155
+ cells = []
156
+ for (const c of data) {
157
+ const b = this.preprocess(c)
158
+ if (b == false) continue
159
+ cells.push(c)
163
160
  }
164
-
165
- if (monitor) monitorDuration('preprocess / filter')
166
- } catch (error) {
167
- //mark as failed
168
- this.cache[xT][yT] = 'failed'
169
- return
170
- }
171
-
172
- //store tile in cache
173
- if (!this.info) {
174
- console.error('Tile info inknown')
175
- return
161
+ } else {
162
+ cells = data
176
163
  }
177
- const tile_ = new GridTile(cells, xT, yT, this.info)
178
- this.cache[xT][yT] = tile_
179
-
180
- if (monitor) monitorDuration('storage')
181
-
182
- //if no redraw is specified, then leave
183
- if (!redrawFun) return
184
-
185
- //check if redraw is really needed, that is if:
186
-
187
- // 1. the dataset belongs to a layer which is visible at the current zoom level
188
- let redraw = false
189
- //go through the layers
190
- const zf = this.app.getZoomFactor()
191
- for (const lay of this.app.layers) {
192
- if (!lay.visible) continue
193
- if (lay.getDatasetComponent(zf) != this) continue
194
- //found one layer. No need to seek more.
195
- redraw = true
196
- break
197
- }
198
- if (monitor) monitorDuration('check redraw 1')
199
-
200
- if (!redraw) return
201
-
202
- // 2. the tile is within the view, that is its geo envelope intersects the viewer geo envelope.
203
- const env = this.app.updateExtentGeo()
204
- const envT = tile_.extGeo
205
- if (env.xMax <= envT.xMin) return
206
- if (env.xMin >= envT.xMax) return
207
- if (env.yMax <= envT.yMin) return
208
- if (env.yMin >= envT.yMax) return
209
-
210
- if (monitor) monitorDuration('check redraw 2')
211
- if (monitor) monitorDuration('*** TiledGrid parse end')
212
164
 
213
- //redraw
214
- redrawFun()
215
- })()
165
+ //if (monitor) monitorDuration('preprocess / filter')
166
+ } catch (error) {
167
+ //mark as failed
168
+ this.cache[xT][yT] = 'failed'
169
+ return
170
+ }
171
+
172
+ //store tile in cache
173
+ if (!this.info) {
174
+ console.error('Tile info inknown')
175
+ return
176
+ }
177
+ const tile_ = new GridTile(cells, xT, yT, this.info)
178
+ this.cache[xT][yT] = tile_
179
+
180
+ //if (monitor) monitorDuration('storage')
181
+
182
+ //if no redraw is specified, then leave
183
+ if (!redrawFun) return
184
+
185
+ //check if redraw is really needed, that is if:
186
+
187
+ // 1. the dataset belongs to a layer which is visible at the current zoom level
188
+ let redraw = false
189
+ //go through the layers
190
+ const zf = this.app.getZoomFactor()
191
+ for (const lay of this.app.layers) {
192
+ if (!lay.visible) continue
193
+ if (lay.getDatasetComponent(zf) != this) continue
194
+ //found one layer. No need to seek more.
195
+ redraw = true
196
+ break
197
+ }
198
+ //if (monitor) monitorDuration('check redraw 1')
199
+
200
+ if (!redraw) return
201
+
202
+ // 2. the tile is within the view, that is its geo envelope intersects the viewer geo envelope.
203
+ const env = this.app.updateExtentGeo()
204
+ const envT = tile_.extGeo
205
+ if (env.xMax <= envT.xMin) return
206
+ if (env.xMin >= envT.xMax) return
207
+ if (env.yMax <= envT.yMin) return
208
+ if (env.yMin >= envT.yMax) return
209
+
210
+ //if (monitor) monitorDuration('check redraw 2')
211
+ //if (monitor) monitorDuration('*** TiledGrid parse end')
212
+
213
+ //redraw
214
+ redrawFun()
215
+ })()
216
216
  }
217
217
  }
218
218
  return this
package/src/index.js CHANGED
@@ -35,6 +35,7 @@ export { SquareColorCatWGLStyle } from './style/SquareColorCatWGLStyle.js'
35
35
  export { MosaicStyle } from './style/MosaicStyle.js'
36
36
  export { NinjaStarStyle } from './style/NinjaStarStyle.js'
37
37
  export { TimeSeriesStyle } from './style/TimeSeriesStyle.js'
38
+ export { IsoFenceStyle } from './style/IsoFenceStyle.js'
38
39
 
39
40
  // export additional layers
40
41
  export { BackgroundLayer } from './BackgroundLayer.js'
@@ -0,0 +1,232 @@
1
+ //@ts-check
2
+ 'use strict'
3
+
4
+ import { Style } from '../Style.js'
5
+
6
+ /** @typedef {{x:number,y:number,or:"v"|"h",c1:import('../Dataset.js').Cell|undefined,c2:import('../Dataset.js').Cell|undefined}} Side */
7
+
8
+ /**
9
+ * @author Julien Gaffuri
10
+ */
11
+ export class IsoFenceStyle extends Style {
12
+
13
+ /** @param {object} opts */
14
+ constructor(opts) {
15
+ super(opts)
16
+ opts = opts || {}
17
+
18
+ /**
19
+ * The dictionary (string -> color) which give the color of each category.
20
+ * @type {object} */
21
+ this.color = opts.color
22
+
23
+ /** The column where to get the height values.
24
+ * @type {string} */
25
+ this.heightCol = opts.heightCol
26
+
27
+ /** A function returning the height of a cell.
28
+ * @type {function(number,number,import("../Style").Stat|undefined,number):number} */
29
+ this.height = opts.height || ((v, r, s, zf) => r * 0.4)
30
+
31
+ /** The perspective angle.
32
+ * @type {number} */
33
+ this.angle = opts.angle != undefined ? opts.angle : 45
34
+
35
+ /** A function returning the corner line stroke style.
36
+ * @type {function(import('../Dataset.js').Cell,number,number):string} */
37
+ this.cornerLineStrokeColor = opts.cornerLineStrokeColor || ((c, r, zf) => "#333")
38
+
39
+ /** A function returning the corner line width.
40
+ * @type {function(import('../Dataset.js').Cell,number,number):number} */
41
+ this.cornerLineWidth = opts.cornerLineWidth || ((c, r, zf) => 0.8 * zf)
42
+ }
43
+
44
+ /**
45
+ * Draw cells as segments.
46
+ *
47
+ * @param {Array.<import("../Dataset.js").Cell>} cells
48
+ * @param {number} r
49
+ * @param {import("../GeoCanvas.js").GeoCanvas} cg
50
+ */
51
+ draw(cells, r, cg) {
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.heightCol) {
60
+ //if size is used, sort cells by size so that the biggest are drawn first
61
+ cells.sort((c1, c2) => c2[this.heightCol] - c1[this.heightCol])
62
+ //and compute statistics
63
+ stat = Style.getStatistics(cells, (c) => c[this.heightCol], true)
64
+ }
65
+
66
+ //nb categories - used for radar and agepyramid
67
+ const cats = Object.keys(this.color)
68
+
69
+ //draw in geo coordinates
70
+ cg.setCanvasTransform()
71
+
72
+ //half resolution
73
+ const r2 = r / 2
74
+
75
+ //height
76
+ /** @type {function(number,number,import("../Style").Stat|undefined,number):number} */
77
+ let h_ = this.height
78
+
79
+ //make sides
80
+ /** @type {Array.<Side>} */
81
+ const sides = []
82
+
83
+ //make horizontal sides
84
+ //sort cells by x and y
85
+ cells.sort((c1, c2) => (c2.x == c1.x ? c1.y - c2.y : c1.x - c2.x))
86
+ let c1 = cells[0]
87
+ for (let i = 1; i < cells.length; i++) {
88
+ let c2 = cells[i]
89
+
90
+ if ((c1.y + r == c2.y) && (c1.x == c2.x))
91
+ //cells in same column and touch along horizontal side
92
+ //make shared side
93
+ sides.push({ x: c1.x + r2, y: c2.y, or: 'h', c1: c1, c2: c2 })
94
+ else {
95
+ //cells do not touch along horizontal side
96
+ //make two sides: top one for c1, bottom for c2
97
+ sides.push({ x: c1.x + r2, y: c1.y + r, or: 'h', c1: c1, c2: undefined })
98
+ sides.push({ x: c2.x + r2, y: c2.y, or: 'h', c1: undefined, c2: c2 })
99
+ }
100
+
101
+ c1 = c2
102
+ }
103
+
104
+ //make vertical sides
105
+ //sort cells by y and x
106
+ cells.sort((c1, c2) => (c2.y == c1.y ? c1.x - c2.x : c1.y - c2.y))
107
+ c1 = cells[0]
108
+ for (let i = 1; i < cells.length; i++) {
109
+ let c2 = cells[i]
110
+
111
+ if ((c1.x + r == c2.x) && (c1.y == c2.y))
112
+ //cells in same row and touch along vertical side
113
+ //make shared side
114
+ sides.push({ x: c2.x, y: c1.y + r2, or: 'v', c1: c1, c2: c2 })
115
+ else {
116
+ //cells do not touch along vertical side
117
+ //make two sides: right one for c1, left for c2
118
+ sides.push({ x: c1.x + r, y: c1.y + r2, or: 'v', c1: c1, c2: undefined })
119
+ sides.push({ x: c2.x, y: c2.y + r2, or: 'v', c1: undefined, c2: c2 })
120
+ }
121
+
122
+ c1 = c2
123
+ }
124
+
125
+ //
126
+ if (sides.length == 0) return
127
+
128
+ //angle in radians
129
+ const aRad = this.angle * Math.PI / 180, cos = Math.cos(aRad), sin = Math.sin(aRad)
130
+
131
+ //draw in geo coordinates
132
+ cg.setCanvasTransform()
133
+
134
+ //sort sides so that the north east ones are drown first
135
+ sides.sort((s1, s2) => (Math.hypot(s2.x, s2.y) - Math.hypot(s1.x, s1.y)))
136
+
137
+ //draw sides
138
+ for (let s of sides) {
139
+
140
+ //heights - in geo
141
+ /** @type {number} */
142
+ const hG1 = s.c1 ? h_(s.c1[this.heightCol], r, stat, zf) : 0
143
+ /** @type {number} */
144
+ const hG2 = s.c2 ? h_(s.c2[this.heightCol], r, stat, zf) : 0
145
+
146
+ //compute totals for both cells
147
+ const total1 = computeTotal(s.c1, cats)
148
+ const total2 = computeTotal(s.c2, cats)
149
+ if (total1 == 0 && total2 == 0) continue
150
+
151
+ let cumul1 = 0, cumul2 = 0
152
+ for (let [column, color] of Object.entries(this.color)) {
153
+ //draw stripe of side s and category column
154
+
155
+ //get values for both cells
156
+ let v1 = s.c1 ? +s.c1[column] : 0
157
+ let v2 = s.c2 ? +s.c2[column] : 0
158
+ if (v1 == 0 && v2 == 0) continue
159
+
160
+ //compute heights
161
+ const h1 = total1 > 0 ? hG1 * cumul1 / total1 : 0
162
+ const h1n = total1 > 0 ? hG1 * (cumul1 + v1) / total1 : 0
163
+ const h2 = total2 > 0 ? hG2 * cumul2 / total2 : 0
164
+ const h2n = total2 > 0 ? hG2 * (cumul2 + v2) / total2 : 0
165
+
166
+ //make path
167
+ cg.ctx.beginPath()
168
+ if (s.or == "h") {
169
+ //horizontal side - vertical section
170
+ //bottom left
171
+ cg.ctx.moveTo(s.x + h1 * cos, s.y - r2 + h1 * sin)
172
+ //top left
173
+ cg.ctx.lineTo(s.x + h2 * cos, s.y + r2 + h2 * sin)
174
+ //top right
175
+ cg.ctx.lineTo(s.x + h2n * cos, s.y + r2 + h2n * sin)
176
+ //bottom right
177
+ cg.ctx.lineTo(s.x + h1n * cos, s.y - r2 + h1n * sin)
178
+ } else {
179
+ //vertical side - horizontal section
180
+ //bottom left
181
+ cg.ctx.moveTo(s.x - r2 + h1 * cos, s.y + h1 * sin)
182
+ //bottom right
183
+ cg.ctx.lineTo(s.x + r2 + h2 * cos, s.y + h2 * sin)
184
+ //top right
185
+ cg.ctx.lineTo(s.x + r2 + h2n * cos, s.y + h2n * sin)
186
+ //top left
187
+ cg.ctx.lineTo(s.x - r2 + h1n * cos, s.y + h1n * sin)
188
+ }
189
+ cg.ctx.closePath()
190
+
191
+ //fill
192
+ cg.ctx.fillStyle = color
193
+ cg.ctx.fill()
194
+
195
+ cumul1 += v1
196
+ cumul2 += v2
197
+ //break
198
+ }
199
+ }
200
+
201
+ //draw corner lines
202
+ for (let c of cells) {
203
+ //height - in geo
204
+ const hG = h_(c[this.heightCol], r, stat, zf)
205
+
206
+ cg.ctx.strokeStyle = this.cornerLineStrokeColor ? this.cornerLineStrokeColor(c, r, zf) : "#333"
207
+ cg.ctx.lineWidth = this.cornerLineWidth ? this.cornerLineWidth(c, r, zf) : 0.8 * zf
208
+
209
+ cg.ctx.beginPath()
210
+ cg.ctx.moveTo(c.x + r2, c.y + r2)
211
+ cg.ctx.lineTo(c.x + r2 + hG * cos, c.y + r2 + hG * sin)
212
+ cg.ctx.closePath()
213
+ cg.ctx.stroke()
214
+ }
215
+
216
+ //update legends
217
+ this.updateLegends({ style: this, r: r, zf: zf, sSize: stat })
218
+ }
219
+ }
220
+
221
+
222
+
223
+ const computeTotal = (cell, categories) => {
224
+ if (!cell) return 0
225
+ let total = 0
226
+ for (let column of categories) {
227
+ const v = cell[column]
228
+ if (!v) continue
229
+ total += +v
230
+ }
231
+ return total || 0
232
+ }
@@ -64,8 +64,6 @@ export class SideStyle extends Style {
64
64
  /** @type {Array.<Side>} */
65
65
  const sides = []
66
66
 
67
- const epsilon = r * 0.001
68
-
69
67
  //make horizontal sides
70
68
  //sort cells by x and y
71
69
  cells.sort((c1, c2) => (c2.x == c1.x ? c1.y - c2.y : c1.x - c2.x))
@@ -73,7 +71,7 @@ export class SideStyle extends Style {
73
71
  for (let i = 1; i < cells.length; i++) {
74
72
  let c2 = cells[i]
75
73
 
76
- if (Math.abs(c1.y + r - c2.y) <= epsilon && Math.abs(c1.x - c2.x) <= epsilon)
74
+ if ((c1.y + r == c2.y) && (c1.x == c2.x))
77
75
  //cells in same column and touch along horizontal side
78
76
  //make shared side
79
77
  sides.push({
@@ -109,7 +107,7 @@ export class SideStyle extends Style {
109
107
  for (let i = 1; i < cells.length; i++) {
110
108
  let c2 = cells[i]
111
109
 
112
- if (Math.abs(c1.x + r - c2.x) <= epsilon && Math.abs(c1.y - c2.y) <= epsilon)
110
+ if ((c1.x + r == c2.x) && (c1.y == c2.y))
113
111
  //cells in same row and touch along vertical side
114
112
  //make shared side
115
113
  sides.push({