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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gridviz",
3
- "version": "2.1.14",
3
+ "version": "2.1.16",
4
4
  "description": "Visualization tool for gridded statistics",
5
5
  "keywords": [
6
6
  "statistics",
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', '#FFFFFFCC')
219
+ .style('background', '#FFFFFF')
218
220
  //.style("padding", this.padding)
219
221
  .style('border', '0px')
220
- .style('border-radius', '5px')
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('top', '20px')
224
- .style('right', '20px')
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.container.addEventListener('mouseover', this.mouseOverHandler)
309
- this.container.addEventListener('mousemove', this.mouseMoveHandler)
310
- this.container.addEventListener('mouseout', this.mouseOutHandler)
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 || this.w - 50,
659
- y: opts?.y || 10,
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 || this.w - 50,
684
- y: opts?.y || 85,
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'] || '5px'
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
- if (this.tooltip.empty())
55
- this.tooltip = select(
56
- '#' + this.parentElement.id && this.parentElement.id != ''
57
- ? '#' + this.parentElement.id
58
- : 'body'
59
- )
60
- .append('div')
61
- .attr('id', this.div)
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)
@@ -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
- // 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)
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.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'})
37
+ if (opts.onClickFunction) this.div.on('click', opts.onClickFunction)
34
38
 
35
39
  //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'
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.node.addEventListener('click', (e) => {
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(containerDiv)
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(containerDiv)
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)=>{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'})
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)=>{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'})
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.node.style.height="unset"
54
- this.node.style.display="unset"
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
+ }