gridviz 2.1.12 → 2.1.14

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.12",
3
+ "version": "2.1.14",
4
4
  "description": "Visualization tool for gridded statistics",
5
5
  "keywords": [
6
6
  "statistics",
@@ -39,7 +39,7 @@
39
39
  "scripts": {
40
40
  "start": "export NODE_OPTIONS=--openssl-legacy-provider && webpack --config webpack.config.dev.cjs",
41
41
  "start-dev": "webpack --config webpack.config.dev.cjs",
42
- "server": "webpack serve -c webpack.config.dev.js --static ./",
42
+ "server": "webpack serve -c webpack.config.dev.cjs",
43
43
  "doc": "./node_modules/.bin/jsdoc src/ src/dataset src/legend src/style -d docs/jsdoc",
44
44
  "bp": "webpack --config webpack.config.prod.cjs",
45
45
  "test": "jest"
@@ -66,9 +66,9 @@
66
66
  "jshint": "^2.12.0",
67
67
  "prettier": "2.8.8",
68
68
  "puppeteer": "^9.1.1",
69
- "webpack": "^5.75.0",
70
- "webpack-cli": "^5.0.0",
71
- "webpack-dev-server": "^4.11.1",
69
+ "webpack": "^5.89.0",
70
+ "webpack-cli": "^5.1.4",
71
+ "webpack-dev-server": "^4.15.1",
72
72
  "webpack-livereload-plugin": "^3.0.2"
73
73
  },
74
74
  "engines": {
package/src/App.js CHANGED
@@ -14,6 +14,8 @@ import { BackgroundLayerWMS } from './BackgroundLayerWMS.js'
14
14
  import { LabelLayer } from './LabelLayer.js'
15
15
  import { LineLayer } from './LineLayer.js'
16
16
  import { monitor, monitorDuration } from './utils/Utils.js'
17
+ import { ZoomButtons } from './button/ZoomButtons.js'
18
+ import { FullscreenButton } from './button/FullscreenButton.js'
17
19
 
18
20
  // external imports
19
21
  import { select } from 'd3-selection'
@@ -326,7 +328,8 @@ export class App {
326
328
  //canvas.addEventListener("keydown", e => { console.log(arguments) });
327
329
 
328
330
  //set default globalCompositeOperation
329
- this.defaultGlobalCompositeOperation = opts.defaultGlobalCompositeOperation || this.cg.ctx.globalCompositeOperation;
331
+ this.defaultGlobalCompositeOperation =
332
+ opts.defaultGlobalCompositeOperation || this.cg.ctx.globalCompositeOperation
330
333
  }
331
334
 
332
335
  /**
@@ -462,10 +465,7 @@ export class App {
462
465
  * @returns {Dataset}
463
466
  */
464
467
  makeLGridDataset(resolution, cells, opts) {
465
- return new Dataset([new LGrid(resolution, cells),],
466
- [],
467
- opts
468
- )
468
+ return new Dataset([new LGrid(resolution, cells)], [], opts)
469
469
  }
470
470
 
471
471
  /**
@@ -638,6 +638,55 @@ export class App {
638
638
  return this
639
639
  }
640
640
 
641
+ /**
642
+ * Adds a set of zoom buttons to the app
643
+ *
644
+ * @param {object} opts
645
+ * @returns {this}
646
+ */
647
+ addZoomButtons(opts) {
648
+ // * opts.id
649
+ // * opts.onZoom - custom event handler function
650
+ // * opts.x
651
+ // * opts.y
652
+ // * opts.delta - zoom delta applied on each click
653
+
654
+ this.zoomButtons = new ZoomButtons({
655
+ app: this,
656
+ id: opts?.id || 'gridviz-zoom-buttons',
657
+ class: opts?.class,
658
+ x: opts?.x || this.w - 50,
659
+ y: opts?.y || 10,
660
+ onZoom: opts?.onZoom,
661
+ delta: opts?.delta || 0.2
662
+ })
663
+
664
+ return this
665
+ }
666
+
667
+ /**
668
+ * Adds a fullscreen toggle button to the app
669
+ *
670
+ * @param {object} opts
671
+ * @returns {this}
672
+ */
673
+ addFullscreenButton(opts) {
674
+ // * opts.app - the gridviz app
675
+ // * opts.id
676
+ // * opts.x
677
+ // * opts.y
678
+
679
+ this.fullscreenButton = new FullscreenButton({
680
+ app: this,
681
+ id: opts?.id || 'gridviz-fullscreen-button',
682
+ class: opts?.class,
683
+ x: opts?.x || this.w - 50,
684
+ y: opts?.y || 85,
685
+ })
686
+
687
+ return this
688
+ }
689
+
641
690
  /** @returns {this} */
642
691
  setViewFromURL() {
643
692
  this.cg.setViewFromURL()
@@ -670,6 +719,10 @@ export class App {
670
719
  canvas.setAttribute('width', '' + this.w)
671
720
  canvas.setAttribute('height', '' + this.h)
672
721
  this.redraw()
722
+
723
+ //update button positions
724
+ if (this.zoomButtons) this.zoomButtons.node.style.left = this.w - 50 + 'px'
725
+ if (this.fullscreenButton) this.fullscreenButton.node.style.left = this.w - 50 + 'px'
673
726
  }
674
727
  })
675
728
  }
package/src/Style.js CHANGED
@@ -10,15 +10,12 @@
10
10
  /**
11
11
  * A style, to show a grid dataset.
12
12
  *
13
- * @abstract
14
- *
15
13
  * @author Joseph Davies, Julien Gaffuri
16
14
  */
17
15
  export class Style {
18
16
  /**
19
17
  * @abstract
20
- * @param {{filter?:function(import('./Dataset').Cell):boolean,offset?:function(import('./Dataset').Cell,number,number):{dx:number,dy:number},visible?:boolean,alpha?:function(number):number,blendOperation?:function(number):GlobalCompositeOperation,minZoom?:number,maxZoom?:number}} opts
21
- * minZoom: The minimum zoom level when to show the layer. maxZoom: The maximum zoom level when to show the layer
18
+ * @param {{filter?:function(import('./Dataset').Cell):boolean,offset?:function(import('./Dataset').Cell,number,number):{dx:number,dy:number},visible?:boolean,alpha?:function(number):number,blendOperation?:function(number):GlobalCompositeOperation,minZoom?:number,maxZoom?:number,drawFun?:function}} opts
22
19
  */
23
20
  constructor(opts) {
24
21
  opts = opts || {}
@@ -58,6 +55,10 @@ export class Style {
58
55
  * */
59
56
  this.maxZoom = opts.maxZoom || Infinity
60
57
 
58
+ /** A draw function for the style.
59
+ * @type {function} */
60
+ this.drawFun = opts.drawFun
61
+
61
62
  //ensure acceptable values for the zoom limits.
62
63
  if (this.minZoom >= this.maxZoom)
63
64
  throw new Error('Unexpected zoom limits for layer. Zoom min should be smaller than zoom max.')
@@ -77,7 +78,8 @@ export class Style {
77
78
  * @abstract
78
79
  */
79
80
  draw(cells, resolution, cg) {
80
- throw new Error('Method draw not implemented.')
81
+ if (this.drawFun) this.drawFun(cells, resolution, cg)
82
+ else throw new Error('Method draw not implemented.')
81
83
  }
82
84
 
83
85
  //getters and setters
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Parent class for button elements used to interact with the gridviz viewer.
3
+ *
4
+ * @author Joseph Davies, Julien Gaffuri
5
+ */
6
+ export class Button {
7
+ /**
8
+ * @param {Object} opts
9
+ * opts.parentNode
10
+ * opts.id
11
+ * opts.title
12
+ * opts.class
13
+ * opts.onClickFunction
14
+ * opts.x
15
+ * opts.y
16
+ */
17
+ constructor(opts) {
18
+ opts = opts || {}
19
+
20
+ this.app = opts.app
21
+ this.parentNode = opts.parentNode || opts.app.container
22
+ this.id = opts.id || 'button-' + Math.random()
23
+
24
+ // create HTML node
25
+ this.node = document.createElement('div')
26
+ this.node.id = this.id
27
+ if (opts.title) this.node.title = opts.title
28
+ if (opts.class) this.node.classList.add(opts.class)
29
+
30
+ // add events
31
+ if (opts.onClickFunction) this.node.addEventListener('onclick', opts.onClickFunction)
32
+ this.node.addEventListener('mouseover', (e)=>{this.node.style.backgroundColor = 'lightgrey'})
33
+ this.node.addEventListener('mouseout', (e)=>{this.node.style.backgroundColor = '#ffffff'})
34
+
35
+ //set styles
36
+ this.node.classList.add(opts.class)
37
+ this.node.style.boxShadow =
38
+ '0 7px 8px rgba(0,47,103,.08), 0 0 22px rgba(0,47,103,.04), 0 12px 17px rgba(0,47,103,.04), 0 -4px 4px rgba(0,47,103,.04)' //.ecl-u-shadow-3
39
+ this.node.style.backgroundColor = '#ffffff' //.ecl-u-bg-white
40
+ this.node.style.position = 'absolute'
41
+ this.node.style.cursor = 'pointer'
42
+ this.node.style.display = 'flex'
43
+ this.node.style.justifyContent = 'center'
44
+ this.node.style.alignItems = 'center'
45
+ this.node.style.width = '30px'
46
+ this.node.style.height = '30px'
47
+ // this.node.style.padding = '4px'
48
+ this.node.style.top = opts.y + 'px'
49
+ this.node.style.left = opts.x + 'px'
50
+
51
+ // append to parent
52
+ this.parentNode.appendChild(this.node)
53
+ }
54
+ }
@@ -0,0 +1,98 @@
1
+ import { Button } from './Button.js'
2
+
3
+ /**
4
+ * Button for toggling fullscreen mode
5
+ *
6
+ * @author Joseph Davies, Julien Gaffuri
7
+ */
8
+ export class FullscreenButton extends Button {
9
+ /**
10
+ * @param {Object} opts
11
+ * opts.parentNode - the node that the button is appended to
12
+ * opts.canvas - the gridviz canvas
13
+ * opts.id
14
+ * opts.title - HTML title attribute
15
+ * opts.class - css class
16
+ * opts.onClickFunction
17
+ * opts.x - x position of the button
18
+ * opts.y - y position of the button
19
+ */
20
+
21
+ // default state
22
+ isFullscreen = false
23
+
24
+ constructor(opts) {
25
+ super(opts)
26
+
27
+ // append fullscreen icon to button container
28
+ this.node.innerHTML = `
29
+ <svg
30
+ style="height: 1.2rem; width: 1.2rem; fill:black; margin:0;"
31
+ focusable="false"
32
+ aria-hidden="true"
33
+ >
34
+ <svg fill="#000000" viewBox="0 0 96 96" xmlns="http://www.w3.org/2000/svg">
35
+ <title/>
36
+ <g>
37
+ <path d="M30,0H6A5.9966,5.9966,0,0,0,0,6V30a6,6,0,0,0,12,0V12H30A6,6,0,0,0,30,0Z"/>
38
+ <path d="M90,0H66a6,6,0,0,0,0,12H84V30a6,6,0,0,0,12,0V6A5.9966,5.9966,0,0,0,90,0Z"/>
39
+ <path d="M30,84H12V66A6,6,0,0,0,0,66V90a5.9966,5.9966,0,0,0,6,6H30a6,6,0,0,0,0-12Z"/>
40
+ <path d="M90,60a5.9966,5.9966,0,0,0-6,6V84H66a6,6,0,0,0,0,12H90a5.9966,5.9966,0,0,0,6-6V66A5.9966,5.9966,0,0,0,90,60Z"/>
41
+ </g>
42
+ </svg>
43
+ </svg>
44
+ `
45
+
46
+ //save initial app dimensions
47
+ this.defaultHeight = this.app.h
48
+ this.defaultWidth = this.app.w
49
+
50
+ // event handler
51
+ this.node.addEventListener('click', (e) => {
52
+ this.onClickFunction(e)
53
+ })
54
+
55
+ }
56
+
57
+ onClickFunction(e) {
58
+ if (this.isFullscreen) {
59
+ this.closeFullscreen(containerDiv)
60
+ //resize canvas to default
61
+ this.app.h = this.defaultHeight
62
+ this.app.w = this.defaultWidth
63
+ this.isFullscreen = false
64
+ } else {
65
+ this.openFullscreen(containerDiv)
66
+ //resize canvas to fullscreen
67
+ this.app.h = window.screen.height
68
+ this.app.w = window.screen.width
69
+ this.isFullscreen = true
70
+ }
71
+ }
72
+
73
+ /* Open fullscreen */
74
+ openFullscreen(elem) {
75
+ if (elem.requestFullscreen) {
76
+ elem.requestFullscreen()
77
+ } else if (elem.webkitRequestFullscreen) {
78
+ /* Safari */
79
+ elem.webkitRequestFullscreen()
80
+ } else if (elem.msRequestFullscreen) {
81
+ /* IE11 */
82
+ elem.msRequestFullscreen()
83
+ }
84
+ }
85
+
86
+ /* Close fullscreen */
87
+ closeFullscreen() {
88
+ if (document.exitFullscreen) {
89
+ document.exitFullscreen()
90
+ } else if (document.webkitExitFullscreen) {
91
+ /* Safari */
92
+ document.webkitExitFullscreen()
93
+ } else if (document.msExitFullscreen) {
94
+ /* IE11 */
95
+ document.msExitFullscreen()
96
+ }
97
+ }
98
+ }
@@ -0,0 +1,72 @@
1
+ import { Button } from './Button.js'
2
+
3
+
4
+ /**
5
+ * Button for toggling fullscreen mode
6
+ *
7
+ * @author Joseph Davies, Julien Gaffuri
8
+ */
9
+ export class ZoomButtons extends Button {
10
+ /**
11
+ * @param {Object} opts
12
+ */
13
+ constructor(opts) {
14
+ super(opts)
15
+
16
+ this.onZoom = opts.onZoom // custom user event handler
17
+ this.delta = opts.delta || 0.2
18
+
19
+ // zoom in btn
20
+ this.zoomInBtn = document.createElement('a')
21
+ this.zoomInBtn.innerHTML = `<a id="zoomin" class="gridviz-zoom-button" title="Zoom in">+</a>`
22
+ this.zoomInBtn.title = 'Zoom in';
23
+ this.zoomInBtn.addEventListener('click', (e)=>{this.zoomIn(e)})
24
+ this.zoomInBtn.addEventListener('mouseover', (e)=>{this.zoomInBtn.style.backgroundColor = 'lightgrey'})
25
+ this.zoomInBtn.addEventListener('mouseout', (e)=>{this.zoomInBtn.style.backgroundColor = '#ffffff'})
26
+
27
+ // zoom out btn
28
+ this.zoomOutBtn = document.createElement('a')
29
+ this.zoomOutBtn.innerHTML = `<a id="zoomin" class="gridviz-zoom-button" title="Zoom out">-</a>`
30
+ this.zoomOutBtn.title = 'Zoom out';
31
+ this.zoomOutBtn.addEventListener('click', (e)=>{this.zoomOut(e)})
32
+ this.zoomOutBtn.addEventListener('mouseover', (e)=>{this.zoomOutBtn.style.backgroundColor = 'lightgrey'})
33
+ this.zoomOutBtn.addEventListener('mouseout', (e)=>{this.zoomOutBtn.style.backgroundColor = '#ffffff'})
34
+
35
+ //set styles
36
+ let btns = [this.zoomInBtn, this.zoomOutBtn]
37
+ btns.forEach((btn, i)=>{
38
+ btn.style.alignItems = 'center';
39
+ btn.style.justifyContent = 'center';
40
+ btn.style.display = 'flex';
41
+ btn.style.border = 'none';
42
+ btn.style.color = 'black';
43
+ btn.style.textAlign = 'center';
44
+ btn.style.textDecoration = 'none';
45
+ btn.style.padding = '4px';
46
+ btn.style.fontSize = '20px';
47
+ btn.style.fontWeight = 'bold';
48
+ btn.style.userSelect = 'none';
49
+ if (i==0) btn.style.borderBottom = '1px solid grey'
50
+ })
51
+
52
+ // unset parent class height
53
+ this.node.style.height="unset"
54
+ this.node.style.display="unset"
55
+
56
+ // append to button container
57
+ this.node.appendChild(this.zoomInBtn)
58
+ this.node.appendChild(this.zoomOutBtn)
59
+ }
60
+
61
+ /* Zoom in */
62
+ zoomIn(e) {
63
+ this.app.setZoomFactor(this.app.getZoomFactor() * (1 - this.delta)).redraw()
64
+ if (this.onZoom) this.onZoom(e)
65
+ }
66
+
67
+ /* Zoom out */
68
+ zoomOut(e) {
69
+ this.app.setZoomFactor(this.app.getZoomFactor() * (1 + this.delta)).redraw()
70
+ if (this.onZoom) this.onZoom(e)
71
+ }
72
+ }
@@ -32,10 +32,13 @@ export class ColorLegend extends Legend {
32
32
 
33
33
  this.fontSize = opts.fontSize || '0.8em'
34
34
  this.invert = opts.invert
35
+
36
+ //to be used as opts => opts.sAlpha to show legend on alpha channel
37
+ this.getStats = opts.getStats || (opts => opts.sColor)
35
38
  }
36
39
 
37
40
  /**
38
- * @param {{ style: import("../Style").Style, r: number, zf: number, sSize: import("../Style").Stat, sColor: import("../Style").Stat }} opts
41
+ * @param {{ style: import("../Style").Style, r: number, zf: number, sSize: import("../Style").Stat, sColor: import("../Style").Stat, sAlpha: import("../Style").Stat }} opts
39
42
  */
40
43
  update(opts) {
41
44
  //could happen when data is still loading
@@ -111,10 +114,11 @@ export class ColorLegend extends Legend {
111
114
 
112
115
  //label text format
113
116
  const f = this.tickFormat && this.tickFormat != 'text' ? format(this.tickFormat) : (v) => v
117
+ const stat = this.getStats(opts)
114
118
  for (let i = 0; i < this.ticks; i++) {
115
119
  let t = i / (this.ticks - 1)
116
120
 
117
- const v = this.fun(t, opts.r, opts.sColor)
121
+ const v = this.fun(t, opts.r, stat)
118
122
  const text = (v ? f(v) : '0') + (this.tickUnit ? this.tickUnit : '')
119
123
 
120
124
  //tick label
@@ -21,16 +21,20 @@ export class SegmentWidthLegend extends Legend {
21
21
  this.titleFontWeight = opts.titleFontWeight || 'bold'
22
22
 
23
23
  //exageration
24
+ //if set to 1, the segment width in the legend will be the one of the maximum width on the map
24
25
  this.exaggerationFactor = opts.exaggerationFactor || 0.5
25
26
 
26
- //color
27
+ //color of the segment in the legend
27
28
  this.color = opts.color || 'gray'
28
- //orientation
29
+ //orientation of the segment in the legend
29
30
  this.orientation = opts.orientation || 0
30
31
 
31
32
  //label
32
33
  this.labelFontSize = opts.labelFontSize || '0.8em'
33
34
  this.labelUnitText = opts.labelUnitText || ''
35
+
36
+ //segment length in geo unit - a function of the resolution r and zoom level zf
37
+ this.lengthFun = opts.lengthExaggerationFactor || ((r, zf) => r)
34
38
  }
35
39
 
36
40
  /**
@@ -54,10 +58,9 @@ export class SegmentWidthLegend extends Legend {
54
58
  .text(this.title)
55
59
  }
56
60
 
57
- //get max value
61
+ //get segment max value
58
62
  const value_ = opts.sWidth.max * this.exaggerationFactor
59
-
60
- //take 'nice' value (power of ten, or multiple)
63
+ //make 'nice' value (power of ten, or multiple)
61
64
  let pow10 = Math.log10(value_)
62
65
  pow10 = Math.floor(pow10)
63
66
  let value = Math.pow(10, pow10)
@@ -67,10 +70,11 @@ export class SegmentWidthLegend extends Legend {
67
70
  else if (value * 4 <= value_) value *= 4
68
71
  else if (value * 2.5 <= value_) value *= 2.5
69
72
  else if (value * 2 <= value_) value *= 2
73
+ else if (value * 1.5 <= value_) value *= 1.5
70
74
 
71
75
  //compute segment width and length, in pix
72
- const sWidth = opts.style.width(value, opts.r, opts.sWidth, opts.zf) / opts.zf
73
- const sLength = (1 * opts.r) / opts.zf
76
+ const sWidth = opts.widthFun(value, opts.r, opts.sWidth, opts.zf) / opts.zf
77
+ const sLength = this.lengthFun(opts.r, opts.zf) / opts.zf
74
78
 
75
79
  //TODO use orientation
76
80
 
@@ -131,11 +131,11 @@ export class SegmentStyle extends Style {
131
131
 
132
132
  //update legend, if any
133
133
  this.updateLegends({
134
- style: this,
134
+ widthFun: this.width,
135
135
  r: r,
136
136
  zf: zf,
137
137
  sColor: statColor,
138
- sLength: statLength,
138
+ //sLength: statLength,
139
139
  sWidth: statWidth,
140
140
  })
141
141
  }
@@ -147,6 +147,6 @@ export class ShapeColorSizeStyle extends Style {
147
147
  }
148
148
 
149
149
  //update legends
150
- this.updateLegends({ style: this, r: r, zf: zf, sSize: statSize, sColor: statColor })
150
+ this.updateLegends({ style: this, r: r, zf: zf, sSize: statSize, sColor: statColor, sAlpha: statAlpha })
151
151
  }
152
152
  }
@@ -50,7 +50,6 @@ export class TimeSeriesStyle extends Style {
50
50
  * @type {function(number,number,import("../Style.js").Stat|undefined,number):number} */
51
51
  this.lineWidth = opts.lineWidth || ((v, r, s, zf) => 1.5 * zf)
52
52
 
53
-
54
53
  /**
55
54
  * @type {string} */
56
55
  this.colorCol = opts.colorCol
@@ -244,6 +243,16 @@ export class TimeSeriesStyle extends Style {
244
243
 
245
244
  }
246
245
 
246
+ //update legend, if any
247
+ this.updateLegends({
248
+ widthFun: this.lineWidth,
249
+ r: r,
250
+ zf: zf,
251
+ sColor: statColor,
252
+ //sLength: statLength,
253
+ sWidth: statWidth,
254
+ })
255
+
247
256
  }
248
257
 
249
258
  }