gridviz 2.1.14 → 2.1.16
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 +721 -169
- package/dist/gridviz.min.js +1 -1
- package/package.json +1 -1
- package/src/App.js +17 -15
- package/src/Tooltip.js +11 -9
- package/src/button/Button.js +36 -24
- package/src/button/FullscreenButton.js +26 -4
- package/src/button/ZoomButtons.js +50 -27
- package/src/index.js +2 -0
- package/src/style/CompositionStyle_.js +293 -0
- package/src/style/ShapeColorSizeStyle_.js +108 -0
- package/src/utils/stretching.js +59 -18
package/package.json
CHANGED
package/src/App.js
CHANGED
|
@@ -46,6 +46,9 @@ export class App {
|
|
|
46
46
|
return
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
//https://css-tricks.com/absolute-positioning-inside-relative-positioning/
|
|
50
|
+
this.container.style.position = "relative"; // container element must have relative positioning
|
|
51
|
+
|
|
49
52
|
//set dimensions
|
|
50
53
|
/** @type {number} */
|
|
51
54
|
this.w = opts.w || this.container.offsetWidth
|
|
@@ -201,8 +204,7 @@ export class App {
|
|
|
201
204
|
/** @type {boolean} */
|
|
202
205
|
this.showBoundaries = true
|
|
203
206
|
|
|
204
|
-
//legend div
|
|
205
|
-
|
|
207
|
+
// legend div
|
|
206
208
|
this.legendDivId = opts.legendDivId || 'gvizLegend'
|
|
207
209
|
this.legend = select('#' + this.legendDivId)
|
|
208
210
|
if (this.legend.empty()) {
|
|
@@ -214,14 +216,14 @@ export class App {
|
|
|
214
216
|
.style('position', 'absolute')
|
|
215
217
|
.style('width', 'auto')
|
|
216
218
|
.style('height', 'auto')
|
|
217
|
-
.style('background', '#
|
|
219
|
+
.style('background', '#FFFFFF')
|
|
218
220
|
//.style("padding", this.padding)
|
|
219
221
|
.style('border', '0px')
|
|
220
|
-
|
|
222
|
+
//.style('border-radius', '5px')
|
|
221
223
|
.style('box-shadow', '3px 3px 3px grey, -3px -3px 3px #ddd')
|
|
222
224
|
.style('font-family', 'Helvetica, Arial, sans-serif')
|
|
223
|
-
.style('
|
|
224
|
-
.style('right', '
|
|
225
|
+
.style('bottom', '15px')
|
|
226
|
+
.style('right', '15px')
|
|
225
227
|
//hide
|
|
226
228
|
//.style("visibility", "hidden")
|
|
227
229
|
}
|
|
@@ -305,9 +307,9 @@ export class App {
|
|
|
305
307
|
this.mouseOverHandler = (e) => focusCell(e)
|
|
306
308
|
this.mouseMoveHandler = (e) => focusCell(e)
|
|
307
309
|
this.mouseOutHandler = (e) => this.tooltip.hide()
|
|
308
|
-
this.
|
|
309
|
-
this.
|
|
310
|
-
this.
|
|
310
|
+
this.cg.canvas.addEventListener('mouseover', this.mouseOverHandler)
|
|
311
|
+
this.cg.canvas.addEventListener('mousemove', this.mouseMoveHandler)
|
|
312
|
+
this.cg.canvas.addEventListener('mouseout', this.mouseOutHandler)
|
|
311
313
|
|
|
312
314
|
// add extra logic to onZoomStartFun
|
|
313
315
|
this.cg.onZoomStartFun = (e) => {
|
|
@@ -655,8 +657,8 @@ export class App {
|
|
|
655
657
|
app: this,
|
|
656
658
|
id: opts?.id || 'gridviz-zoom-buttons',
|
|
657
659
|
class: opts?.class,
|
|
658
|
-
x: opts?.x
|
|
659
|
-
y: opts?.y
|
|
660
|
+
x: opts?.x,
|
|
661
|
+
y: opts?.y,
|
|
660
662
|
onZoom: opts?.onZoom,
|
|
661
663
|
delta: opts?.delta || 0.2
|
|
662
664
|
})
|
|
@@ -680,8 +682,8 @@ export class App {
|
|
|
680
682
|
app: this,
|
|
681
683
|
id: opts?.id || 'gridviz-fullscreen-button',
|
|
682
684
|
class: opts?.class,
|
|
683
|
-
x: opts?.x
|
|
684
|
-
y: opts?.y
|
|
685
|
+
x: opts?.x,
|
|
686
|
+
y: opts?.y
|
|
685
687
|
})
|
|
686
688
|
|
|
687
689
|
return this
|
|
@@ -721,8 +723,8 @@ export class App {
|
|
|
721
723
|
this.redraw()
|
|
722
724
|
|
|
723
725
|
//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'
|
|
726
|
+
// if (this.zoomButtons) this.zoomButtons.node.style.left = this.w - 50 + 'px'
|
|
727
|
+
// if (this.fullscreenButton) this.fullscreenButton.node.style.left = this.w - 50 + 'px'
|
|
726
728
|
}
|
|
727
729
|
})
|
|
728
730
|
}
|
package/src/Tooltip.js
CHANGED
|
@@ -28,7 +28,7 @@ export class Tooltip {
|
|
|
28
28
|
/** @type {string} */
|
|
29
29
|
this.border = opts.border || '0px'
|
|
30
30
|
/** @type {string} */
|
|
31
|
-
this['border-radius'] = opts['border-radius'] || '
|
|
31
|
+
this['border-radius'] = opts['border-radius'] || '0px'
|
|
32
32
|
/** @type {string} */
|
|
33
33
|
this['box-shadow'] = opts['box-shadow'] || '5px 5px 5px grey'
|
|
34
34
|
/** @type {string} */
|
|
@@ -51,14 +51,16 @@ export class Tooltip {
|
|
|
51
51
|
* @public
|
|
52
52
|
* @type {import("d3-selection").Selection} */
|
|
53
53
|
this.tooltip = select('#' + this.div)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
54
|
+
|
|
55
|
+
if (this.tooltip.empty()) {
|
|
56
|
+
//create tooltip DOM node
|
|
57
|
+
// this.tooltip = select(
|
|
58
|
+
// '#' + this.parentElement.id && this.parentElement.id != ''
|
|
59
|
+
// ? '#' + this.parentElement.id
|
|
60
|
+
// : 'body'
|
|
61
|
+
// )
|
|
62
|
+
this.tooltip = select('body').append('div').attr('id', this.div)
|
|
63
|
+
}
|
|
62
64
|
|
|
63
65
|
//initialise
|
|
64
66
|
this.tooltip.style('max-width', this.maxWidth)
|
package/src/button/Button.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { select } from 'd3-selection'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Parent class for button elements used to interact with the gridviz viewer.
|
|
3
5
|
*
|
|
@@ -19,36 +21,46 @@ export class Button {
|
|
|
19
21
|
|
|
20
22
|
this.app = opts.app
|
|
21
23
|
this.parentNode = opts.parentNode || opts.app.container
|
|
22
|
-
this.id = opts.id || 'button-' + Math.random()
|
|
23
24
|
|
|
24
|
-
//
|
|
25
|
-
this.
|
|
26
|
-
|
|
27
|
-
if (
|
|
28
|
-
|
|
25
|
+
// the div element
|
|
26
|
+
if (this.id) this.div = select('#' + this.id)
|
|
27
|
+
|
|
28
|
+
if (!this.div || this.div.empty()) {
|
|
29
|
+
this.div = select(document.createElement('div'))
|
|
30
|
+
if (this.id) this.div.attr('id', this.id)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (opts.title) this.div.attr('title',opts.title)
|
|
34
|
+
if (opts.class) this.div.attr('class',opts.class)
|
|
29
35
|
|
|
30
36
|
// add events
|
|
31
|
-
if (opts.onClickFunction) this.
|
|
32
|
-
this.node.addEventListener('mouseover', (e)=>{this.node.style.backgroundColor = 'lightgrey'})
|
|
33
|
-
this.node.addEventListener('mouseout', (e)=>{this.node.style.backgroundColor = '#ffffff'})
|
|
37
|
+
if (opts.onClickFunction) this.div.on('click', opts.onClickFunction)
|
|
34
38
|
|
|
35
39
|
//set styles
|
|
36
|
-
this.
|
|
37
|
-
this.
|
|
38
|
-
|
|
39
|
-
this.
|
|
40
|
-
this.
|
|
41
|
-
this.
|
|
42
|
-
|
|
43
|
-
this.
|
|
44
|
-
this.
|
|
45
|
-
this.
|
|
46
|
-
|
|
47
|
-
// this.node.style.padding = '4px'
|
|
48
|
-
this.node.style.top = opts.y + 'px'
|
|
49
|
-
this.node.style.left = opts.x + 'px'
|
|
40
|
+
this.style('box-shadow','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
|
|
41
|
+
this.style('background-color','#ffffff')
|
|
42
|
+
this.style('position' , 'absolute')
|
|
43
|
+
this.style('cursor' , 'pointer')
|
|
44
|
+
this.style('display' , 'flex')
|
|
45
|
+
this.style('justify-content' , 'center')
|
|
46
|
+
this.style('align-items' , 'center')
|
|
47
|
+
this.style('width' , '30px')
|
|
48
|
+
this.style('height' , '30px')
|
|
49
|
+
// this.style(padding , '4px'
|
|
50
|
+
|
|
50
51
|
|
|
51
52
|
// append to parent
|
|
52
|
-
this.parentNode.appendChild(this.node)
|
|
53
|
+
this.parentNode.appendChild(this.div.node())
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Apply a style to the button div.
|
|
58
|
+
* @param {string} k
|
|
59
|
+
* @param {string} v
|
|
60
|
+
* @returns {this}
|
|
61
|
+
*/
|
|
62
|
+
style(k, v) {
|
|
63
|
+
this.div.style(k, v)
|
|
64
|
+
return this
|
|
53
65
|
}
|
|
54
66
|
}
|
|
@@ -25,7 +25,7 @@ export class FullscreenButton extends Button {
|
|
|
25
25
|
super(opts)
|
|
26
26
|
|
|
27
27
|
// append fullscreen icon to button container
|
|
28
|
-
this.node.innerHTML = `
|
|
28
|
+
this.div.node().innerHTML = `
|
|
29
29
|
<svg
|
|
30
30
|
style="height: 1.2rem; width: 1.2rem; fill:black; margin:0;"
|
|
31
31
|
focusable="false"
|
|
@@ -48,21 +48,43 @@ export class FullscreenButton extends Button {
|
|
|
48
48
|
this.defaultWidth = this.app.w
|
|
49
49
|
|
|
50
50
|
// event handler
|
|
51
|
-
this.
|
|
51
|
+
this.div.on('click', (e) => {
|
|
52
52
|
this.onClickFunction(e)
|
|
53
53
|
})
|
|
54
|
+
this.div.on('mouseover', (e) => {
|
|
55
|
+
this.style('background-color', 'lightgrey')
|
|
56
|
+
})
|
|
57
|
+
this.div.on('mouseout', (e) => {
|
|
58
|
+
this.style('background-color', '#ffffff')
|
|
59
|
+
})
|
|
54
60
|
|
|
61
|
+
//set position
|
|
62
|
+
if (opts.x) {
|
|
63
|
+
this.style('left', opts.x + 'px')
|
|
64
|
+
} else {
|
|
65
|
+
this.style('right', '10px')
|
|
66
|
+
}
|
|
67
|
+
if (opts.y) {
|
|
68
|
+
this.style('top', opts.y + 'px')
|
|
69
|
+
} else {
|
|
70
|
+
this.style('top', '90px')
|
|
71
|
+
}
|
|
55
72
|
}
|
|
56
73
|
|
|
57
74
|
onClickFunction(e) {
|
|
58
75
|
if (this.isFullscreen) {
|
|
59
|
-
this.closeFullscreen(
|
|
76
|
+
this.closeFullscreen(this.app.container)
|
|
60
77
|
//resize canvas to default
|
|
61
78
|
this.app.h = this.defaultHeight
|
|
62
79
|
this.app.w = this.defaultWidth
|
|
80
|
+
this.app.cg.h = this.defaultHeight
|
|
81
|
+
this.app.cg.w = this.defaultWidth
|
|
82
|
+
this.app.cg.canvas.setAttribute('width', '' + this.defaultWidth)
|
|
83
|
+
this.app.cg.canvas.setAttribute('height', '' + this.defaultHeight)
|
|
84
|
+
this.app.redraw()
|
|
63
85
|
this.isFullscreen = false
|
|
64
86
|
} else {
|
|
65
|
-
this.openFullscreen(
|
|
87
|
+
this.openFullscreen(this.app.container)
|
|
66
88
|
//resize canvas to fullscreen
|
|
67
89
|
this.app.h = window.screen.height
|
|
68
90
|
this.app.w = window.screen.width
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Button } from './Button.js'
|
|
2
2
|
|
|
3
|
-
|
|
4
3
|
/**
|
|
5
4
|
* Button for toggling fullscreen mode
|
|
6
5
|
*
|
|
@@ -19,43 +18,67 @@ export class ZoomButtons extends Button {
|
|
|
19
18
|
// zoom in btn
|
|
20
19
|
this.zoomInBtn = document.createElement('a')
|
|
21
20
|
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)=>{
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
this.zoomInBtn.title = 'Zoom in'
|
|
22
|
+
this.zoomInBtn.addEventListener('click', (e) => {
|
|
23
|
+
this.zoomIn(e)
|
|
24
|
+
})
|
|
25
|
+
this.zoomInBtn.addEventListener('mouseover', (e) => {
|
|
26
|
+
this.zoomInBtn.style.backgroundColor = 'lightgrey'
|
|
27
|
+
})
|
|
28
|
+
this.zoomInBtn.addEventListener('mouseout', (e) => {
|
|
29
|
+
this.zoomInBtn.style.backgroundColor = '#ffffff'
|
|
30
|
+
})
|
|
26
31
|
|
|
27
32
|
// zoom out btn
|
|
28
33
|
this.zoomOutBtn = document.createElement('a')
|
|
29
34
|
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)=>{
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
this.zoomOutBtn.title = 'Zoom out'
|
|
36
|
+
this.zoomOutBtn.addEventListener('click', (e) => {
|
|
37
|
+
this.zoomOut(e)
|
|
38
|
+
})
|
|
39
|
+
this.zoomOutBtn.addEventListener('mouseover', (e) => {
|
|
40
|
+
this.zoomOutBtn.style.backgroundColor = 'lightgrey'
|
|
41
|
+
})
|
|
42
|
+
this.zoomOutBtn.addEventListener('mouseout', (e) => {
|
|
43
|
+
this.zoomOutBtn.style.backgroundColor = '#ffffff'
|
|
44
|
+
})
|
|
34
45
|
|
|
35
46
|
//set styles
|
|
36
47
|
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'
|
|
48
|
+
btns.forEach((btn, i) => {
|
|
49
|
+
btn.style.alignItems = 'center'
|
|
50
|
+
btn.style.justifyContent = 'center'
|
|
51
|
+
btn.style.display = 'flex'
|
|
52
|
+
btn.style.border = 'none'
|
|
53
|
+
btn.style.color = 'black'
|
|
54
|
+
btn.style.textAlign = 'center'
|
|
55
|
+
btn.style.textDecoration = 'none'
|
|
56
|
+
btn.style.padding = '4px'
|
|
57
|
+
btn.style.fontSize = '20px'
|
|
58
|
+
btn.style.fontWeight = 'bold'
|
|
59
|
+
btn.style.userSelect = 'none'
|
|
60
|
+
if (i == 0) btn.style.borderBottom = '1px solid grey'
|
|
50
61
|
})
|
|
51
62
|
|
|
52
|
-
// unset parent class height
|
|
53
|
-
this.
|
|
54
|
-
this.
|
|
63
|
+
// unset parent class height and display for dual buttons
|
|
64
|
+
this.style('height', 'unset')
|
|
65
|
+
this.style('display', 'unset')
|
|
66
|
+
|
|
67
|
+
//set position
|
|
68
|
+
if (opts.x) {
|
|
69
|
+
this.style('left', opts.x + 'px')
|
|
70
|
+
} else {
|
|
71
|
+
this.style('right', '10px')
|
|
72
|
+
}
|
|
73
|
+
if (opts.y) {
|
|
74
|
+
this.style('top', opts.y + 'px')
|
|
75
|
+
} else {
|
|
76
|
+
this.style('top', '10px')
|
|
77
|
+
}
|
|
55
78
|
|
|
56
79
|
// append to button container
|
|
57
|
-
this.node.appendChild(this.zoomInBtn)
|
|
58
|
-
this.node.appendChild(this.zoomOutBtn)
|
|
80
|
+
this.div.node().appendChild(this.zoomInBtn)
|
|
81
|
+
this.div.node().appendChild(this.zoomOutBtn)
|
|
59
82
|
}
|
|
60
83
|
|
|
61
84
|
/* Zoom in */
|
package/src/index.js
CHANGED
|
@@ -18,9 +18,11 @@ export { LGrid } from './dataset/LGrid.js'
|
|
|
18
18
|
|
|
19
19
|
// export styles
|
|
20
20
|
export { ShapeColorSizeStyle } from './style/ShapeColorSizeStyle.js'
|
|
21
|
+
export { ShapeColorSizeStyle_ } from './style/ShapeColorSizeStyle_.js'
|
|
21
22
|
export { StrokeStyle } from './style/StrokeStyle.js'
|
|
22
23
|
export { JoyPlotStyle } from './style/JoyPlotStyle.js'
|
|
23
24
|
export { CompositionStyle } from './style/CompositionStyle.js'
|
|
25
|
+
export { CompositionStyle_ } from './style/CompositionStyle_.js'
|
|
24
26
|
export { SegmentStyle } from './style/SegmentStyle.js'
|
|
25
27
|
export { TextStyle } from './style/TextStyle.js'
|
|
26
28
|
export { PillarStyle } from './style/PillarStyle.js'
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
import { Style } from '../Style.js'
|
|
5
|
+
|
|
6
|
+
/** @typedef {"flag"|"piechart"|"ring"|"segment"|"radar"|"agepyramid"|"halftone"} CompositionType */
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A style showing the composition of a total in different categories, with different color hues.
|
|
10
|
+
* It consists of a symbol with different parts, whose size reflect the proportion of the corresponding category.
|
|
11
|
+
* For a list of supported symbols, @see CompositionType
|
|
12
|
+
* The symbol can be scaled depending on the cell importance.
|
|
13
|
+
*
|
|
14
|
+
* @author Julien Gaffuri
|
|
15
|
+
*/
|
|
16
|
+
export class CompositionStyle_ extends Style {
|
|
17
|
+
/** @param {object} opts */
|
|
18
|
+
constructor(opts) {
|
|
19
|
+
super(opts)
|
|
20
|
+
opts = opts || {}
|
|
21
|
+
|
|
22
|
+
/** A function returning the view scale.
|
|
23
|
+
* @type {function(Array.<import('../Dataset.js').Cell>,number, number):object} */
|
|
24
|
+
this.viewScale = opts.viewScale
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The dictionary (string -> color) which give the color of each category.
|
|
28
|
+
* @type {object} */
|
|
29
|
+
this.color = opts.color
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A function returning the type of decomposition symbol of a cell, @see CompositionType
|
|
33
|
+
* @type {function(import("../Dataset.js").Cell,number, number,object):CompositionType} */
|
|
34
|
+
this.type = opts.type
|
|
35
|
+
|
|
36
|
+
/** A function returning the size of a cell in geographical unit.
|
|
37
|
+
* @type {function(import('../Dataset.js').Cell,number, number,object):number} */
|
|
38
|
+
this.size = opts.size
|
|
39
|
+
|
|
40
|
+
/** For style types with stripes (flag, segment), the orientation of the stripes (0 for horizontal, other for vertical).
|
|
41
|
+
* @type {function(import("../Dataset.js").Cell,number,number,object):number} */
|
|
42
|
+
this.stripesOrientation = opts.stripesOrientation || (() => 0) //(c,r,zf,vc) => ...
|
|
43
|
+
|
|
44
|
+
/** The function specifying an offset angle for a radar, halftone or pie chart style.
|
|
45
|
+
* The angle is specified in degree. The rotation is anti-clockwise.
|
|
46
|
+
* @type {function(import("../Dataset.js").Cell,number,number,object):number} */
|
|
47
|
+
this.offsetAngle = opts.offsetAngle || (() => 0) //(cell,r,zf,vc) => ...
|
|
48
|
+
|
|
49
|
+
/** The function specifying the height of the age pyramid, in geo unit.
|
|
50
|
+
* @type {function(import("../Dataset.js").Cell,number,number,object):number} */
|
|
51
|
+
this.agePyramidHeight = opts.agePyramidHeight || ((c, r, zf) => r) //(cell,r,zf,vc) => ...
|
|
52
|
+
|
|
53
|
+
/** For pie chart, this is parameter for internal radius, so that the pie chart looks like a donut.
|
|
54
|
+
* 0 for normal pie charts, 0.5 to empty half of the radius.
|
|
55
|
+
* @type {number} */
|
|
56
|
+
this.pieChartInternalRadiusFactor = opts.pieChartInternalRadiusFactor || 0
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Draw cells as squares depending on their value.
|
|
61
|
+
*
|
|
62
|
+
* @param {Array.<import("../Dataset.js").Cell>} cells
|
|
63
|
+
* @param {number} r
|
|
64
|
+
* @param {import("../GeoCanvas.js").GeoCanvas} cg
|
|
65
|
+
*/
|
|
66
|
+
draw(cells, r, cg) {
|
|
67
|
+
//filter
|
|
68
|
+
if (this.filter) cells = cells.filter(this.filter)
|
|
69
|
+
|
|
70
|
+
//zoom factor
|
|
71
|
+
const zf = cg.getZf()
|
|
72
|
+
|
|
73
|
+
//get view scale
|
|
74
|
+
const vc = this.viewScale ? this.viewScale(cells, r, zf) : undefined
|
|
75
|
+
|
|
76
|
+
//nb categories - used for radar and agepyramid
|
|
77
|
+
const nbCat = Object.entries(this.color).length
|
|
78
|
+
|
|
79
|
+
//draw in geo coordinates
|
|
80
|
+
cg.setCanvasTransform()
|
|
81
|
+
|
|
82
|
+
//draw calls
|
|
83
|
+
for (let cell of cells) {
|
|
84
|
+
|
|
85
|
+
//size
|
|
86
|
+
const sG = this.size ? this.size(cell, r, zf, vc) : r
|
|
87
|
+
if (!sG) continue
|
|
88
|
+
|
|
89
|
+
//get offset
|
|
90
|
+
const offset = this.offset(cell, r, zf)
|
|
91
|
+
|
|
92
|
+
//get symbol type
|
|
93
|
+
const type_ = this.type ? this.type(cell, r, zf, vc) : 'flag'
|
|
94
|
+
|
|
95
|
+
//compute center position
|
|
96
|
+
const xc = cell.x + offset.dx + (type_ === 'agepyramid' ? 0 : r * 0.5)
|
|
97
|
+
const yc = cell.y + offset.dy + (type_ === 'agepyramid' ? 0 : r * 0.5)
|
|
98
|
+
|
|
99
|
+
//compute offset angle, when relevant
|
|
100
|
+
const offAng = this.offsetAngle ? (this.offsetAngle(cell, r, zf, vc) * Math.PI) / 180 : 0
|
|
101
|
+
|
|
102
|
+
if (type_ === 'agepyramid' || type_ === 'radar' || type_ === 'halftone') {
|
|
103
|
+
//get cell category max value
|
|
104
|
+
let maxVal = -Infinity
|
|
105
|
+
for (let key of Object.keys(this.color)) {
|
|
106
|
+
const v = +cell[key]
|
|
107
|
+
if (v > maxVal) maxVal = v
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
//cumul
|
|
111
|
+
let cumul = 0
|
|
112
|
+
if (type_ === 'agepyramid' && this.agePyramidHeight)
|
|
113
|
+
cumul = (r - this.agePyramidHeight(cell, r, zf, vc)) / 2
|
|
114
|
+
if (type_ === 'radar' || type_ === 'halftone') cumul = Math.PI / 2 + offAng
|
|
115
|
+
|
|
116
|
+
//compute the increment, which is the value to increment the cumul for each category
|
|
117
|
+
const incr =
|
|
118
|
+
type_ === 'agepyramid'
|
|
119
|
+
? (this.agePyramidHeight ? this.agePyramidHeight(cell, r, zf, vc) : r) / nbCat
|
|
120
|
+
: type_ === 'radar' || type_ === 'halftone'
|
|
121
|
+
? (2 * Math.PI) / nbCat
|
|
122
|
+
: undefined
|
|
123
|
+
if (incr === undefined) throw new Error('Unexpected symbol type:' + type_)
|
|
124
|
+
|
|
125
|
+
for (let [column, color] of Object.entries(this.color)) {
|
|
126
|
+
if (type_ === 'agepyramid') {
|
|
127
|
+
//set category color
|
|
128
|
+
cg.ctx.fillStyle = color
|
|
129
|
+
|
|
130
|
+
//get category value
|
|
131
|
+
const val = cell[column]
|
|
132
|
+
|
|
133
|
+
//compute category length - in geo
|
|
134
|
+
/** @type {number} */
|
|
135
|
+
const wG = (sG * val) / maxVal
|
|
136
|
+
|
|
137
|
+
//draw bar
|
|
138
|
+
cg.ctx.fillRect(xc + (r - wG) / 2, yc + cumul, wG, incr)
|
|
139
|
+
|
|
140
|
+
//next height
|
|
141
|
+
cumul += incr
|
|
142
|
+
} else if (type_ === 'radar') {
|
|
143
|
+
//set category color
|
|
144
|
+
cg.ctx.fillStyle = color
|
|
145
|
+
|
|
146
|
+
//get categroy value
|
|
147
|
+
const val = cell[column]
|
|
148
|
+
|
|
149
|
+
//compute category radius - in geo
|
|
150
|
+
/** @type {number} */
|
|
151
|
+
//const rG = this.radius(val, r, stat, cellStat, zf)
|
|
152
|
+
const rG = (sG / 2) * Math.sqrt(val / maxVal)
|
|
153
|
+
|
|
154
|
+
//draw angular sector
|
|
155
|
+
cg.ctx.beginPath()
|
|
156
|
+
cg.ctx.moveTo(xc, yc)
|
|
157
|
+
cg.ctx.arc(xc, yc, rG, cumul - incr, cumul)
|
|
158
|
+
cg.ctx.lineTo(xc, yc)
|
|
159
|
+
cg.ctx.fill()
|
|
160
|
+
|
|
161
|
+
//next angular sector
|
|
162
|
+
cumul += incr
|
|
163
|
+
} else if (type_ === 'halftone') {
|
|
164
|
+
//set category color
|
|
165
|
+
cg.ctx.fillStyle = color
|
|
166
|
+
|
|
167
|
+
//get categroy value
|
|
168
|
+
const val = cell[column]
|
|
169
|
+
|
|
170
|
+
//compute category radius - in geo
|
|
171
|
+
/** @type {number} */
|
|
172
|
+
const rG = sG * 0.333 * Math.sqrt(val / maxVal)
|
|
173
|
+
|
|
174
|
+
//draw circle
|
|
175
|
+
cg.ctx.beginPath()
|
|
176
|
+
cg.ctx.arc(
|
|
177
|
+
xc + r * 0.25 * Math.cos(cumul),
|
|
178
|
+
yc + r * 0.25 * Math.sin(cumul),
|
|
179
|
+
rG,
|
|
180
|
+
0,
|
|
181
|
+
2 * Math.PI
|
|
182
|
+
)
|
|
183
|
+
cg.ctx.fill()
|
|
184
|
+
|
|
185
|
+
//next angular sector
|
|
186
|
+
cumul += incr
|
|
187
|
+
} else {
|
|
188
|
+
throw new Error('Unexpected symbol type:' + type_)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
//compute total
|
|
193
|
+
let total = 0
|
|
194
|
+
for (let column of Object.keys(this.color)) {
|
|
195
|
+
const v = +cell[column]
|
|
196
|
+
if (!v) continue
|
|
197
|
+
total += v
|
|
198
|
+
}
|
|
199
|
+
if (!total || isNaN(total)) continue
|
|
200
|
+
|
|
201
|
+
//draw decomposition symbol
|
|
202
|
+
let cumul = 0
|
|
203
|
+
const d = r * (1 - sG / r) * 0.5
|
|
204
|
+
const ori = this.stripesOrientation(cell, r, zf, vc)
|
|
205
|
+
|
|
206
|
+
for (let [column, color] of Object.entries(this.color)) {
|
|
207
|
+
//get share
|
|
208
|
+
const share = cell[column] / total
|
|
209
|
+
if (!share || isNaN(share)) continue
|
|
210
|
+
|
|
211
|
+
//set color
|
|
212
|
+
cg.ctx.fillStyle = color
|
|
213
|
+
|
|
214
|
+
//draw symbol part
|
|
215
|
+
if (type_ === 'flag') {
|
|
216
|
+
//draw flag stripe
|
|
217
|
+
if (ori == 0) {
|
|
218
|
+
//horizontal
|
|
219
|
+
cg.ctx.fillRect(
|
|
220
|
+
cell.x + d + offset.dx,
|
|
221
|
+
cell.y + d + cumul * sG + offset.dy,
|
|
222
|
+
sG,
|
|
223
|
+
share * sG
|
|
224
|
+
)
|
|
225
|
+
} else {
|
|
226
|
+
//vertical
|
|
227
|
+
cg.ctx.fillRect(
|
|
228
|
+
cell.x + d + cumul * sG + offset.dx,
|
|
229
|
+
cell.y + d + offset.dy,
|
|
230
|
+
share * sG,
|
|
231
|
+
sG
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
} else if (type_ === 'piechart') {
|
|
235
|
+
//draw pie chart angular sector
|
|
236
|
+
|
|
237
|
+
//compute angles
|
|
238
|
+
const a1 = cumul * 2 * Math.PI
|
|
239
|
+
const a2 = (cumul + share) * 2 * Math.PI
|
|
240
|
+
|
|
241
|
+
//draw
|
|
242
|
+
cg.ctx.beginPath()
|
|
243
|
+
cg.ctx.moveTo(xc, yc)
|
|
244
|
+
cg.ctx.arc(xc, yc, sG * 0.5, a1 + offAng, a2 + offAng)
|
|
245
|
+
if (this.pieChartInternalRadiusFactor)
|
|
246
|
+
cg.ctx.arc(
|
|
247
|
+
xc,
|
|
248
|
+
yc,
|
|
249
|
+
sG * 0.5 * this.pieChartInternalRadiusFactor,
|
|
250
|
+
a1 + offAng,
|
|
251
|
+
a2 + offAng,
|
|
252
|
+
true
|
|
253
|
+
)
|
|
254
|
+
cg.ctx.closePath()
|
|
255
|
+
cg.ctx.fill()
|
|
256
|
+
} else if (type_ === 'ring') {
|
|
257
|
+
//draw ring
|
|
258
|
+
cg.ctx.beginPath()
|
|
259
|
+
cg.ctx.arc(xc, yc, Math.sqrt(1 - cumul) * sG * 0.5, 0, 2 * Math.PI)
|
|
260
|
+
cg.ctx.fill()
|
|
261
|
+
} else if (type_ === 'segment') {
|
|
262
|
+
//draw segment sections
|
|
263
|
+
const wG = (sG * sG) / r
|
|
264
|
+
if (ori == 0) {
|
|
265
|
+
//horizontal
|
|
266
|
+
cg.ctx.fillRect(
|
|
267
|
+
cell.x + offset.dx,
|
|
268
|
+
cell.y + (r - wG) / 2 + cumul * wG + offset.dy,
|
|
269
|
+
r,
|
|
270
|
+
share * wG
|
|
271
|
+
)
|
|
272
|
+
} else {
|
|
273
|
+
//vertical
|
|
274
|
+
cg.ctx.fillRect(
|
|
275
|
+
cell.x + cumul * r + offset.dx,
|
|
276
|
+
cell.y + (r - wG) / 2 + offset.dy,
|
|
277
|
+
share * r,
|
|
278
|
+
wG
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
throw new Error('Unexpected symbol type:' + type_)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
cumul += share
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
//update legends
|
|
291
|
+
this.updateLegends({ style: this, r: r, zf: zf, viewScale: vc })
|
|
292
|
+
}
|
|
293
|
+
}
|