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,142 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
import { Style } from '../Style'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A style where each cell is represented by a segment whose length, width, color and orientation can vary according to statistical values.
|
|
8
|
+
*
|
|
9
|
+
* @author Julien Gaffuri
|
|
10
|
+
*/
|
|
11
|
+
export class SegmentStyle extends Style {
|
|
12
|
+
/** @param {object} opts */
|
|
13
|
+
constructor(opts) {
|
|
14
|
+
super(opts)
|
|
15
|
+
opts = opts || {}
|
|
16
|
+
|
|
17
|
+
/** A function returning the orientation (in degrees) of the segment representing a cell.
|
|
18
|
+
* @type {function(import("../Dataset").Cell):number} */
|
|
19
|
+
this.orientation = opts.orientation || (() => 0)
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @type {string} */
|
|
23
|
+
this.colorCol = opts.colorCol
|
|
24
|
+
|
|
25
|
+
/** A function returning the color of the cell segment.
|
|
26
|
+
* @type {function(number,number,import("../Style").Stat|undefined):string} */
|
|
27
|
+
this.color = opts.color || (() => '#EA6BAC')
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @type {string} */
|
|
31
|
+
this.lengthCol = opts.lengthCol
|
|
32
|
+
|
|
33
|
+
/** A function returning the length of the segment representing a cell, in geo unit
|
|
34
|
+
* @type {function(number,number,import("../Style").Stat|undefined,number):number} */
|
|
35
|
+
this.length = opts.length
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @type {string} */
|
|
39
|
+
this.widthCol = opts.widthCol
|
|
40
|
+
|
|
41
|
+
/** A function returning the width of the segment representing a cell, in geo unit
|
|
42
|
+
* @type {function(number,number,import("../Style").Stat|undefined,number):number} */
|
|
43
|
+
this.width = opts.width
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Draw cells as segments.
|
|
48
|
+
*
|
|
49
|
+
* @param {Array.<import("../Dataset").Cell>} cells
|
|
50
|
+
* @param {number} r
|
|
51
|
+
* @param {import("../GeoCanvas").GeoCanvas} cg
|
|
52
|
+
*/
|
|
53
|
+
draw(cells, r, cg) {
|
|
54
|
+
//filter
|
|
55
|
+
if (this.filter) cells = cells.filter(this.filter)
|
|
56
|
+
|
|
57
|
+
//zoom factor
|
|
58
|
+
const zf = cg.getZf()
|
|
59
|
+
|
|
60
|
+
let statColor
|
|
61
|
+
if (this.colorCol) {
|
|
62
|
+
//compute color variable statistics
|
|
63
|
+
statColor = Style.getStatistics(cells, (c) => c[this.colorCol], true)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let statLength
|
|
67
|
+
if (this.lengthCol) {
|
|
68
|
+
//if length is used, sort cells by length so that the longests are drawn first
|
|
69
|
+
cells.sort((c1, c2) => c2[this.lengthCol] - c1[this.lengthCol])
|
|
70
|
+
//and compute size variable statistics
|
|
71
|
+
statLength = Style.getStatistics(cells, (c) => c[this.lengthCol], true)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let statWidth
|
|
75
|
+
if (this.widthCol) {
|
|
76
|
+
//and compute size variable statistics
|
|
77
|
+
statWidth = Style.getStatistics(cells, (c) => c[this.widthCol], true)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
//
|
|
81
|
+
cg.ctx.lineCap = 'butt'
|
|
82
|
+
|
|
83
|
+
//conversion factor degree -> radian
|
|
84
|
+
const f = Math.PI / 180
|
|
85
|
+
|
|
86
|
+
//draw in geo coordinates
|
|
87
|
+
cg.setCanvasTransform()
|
|
88
|
+
|
|
89
|
+
for (let c of cells) {
|
|
90
|
+
//color
|
|
91
|
+
/** @type {string|undefined} */
|
|
92
|
+
const col = this.color ? this.color(c[this.colorCol], r, statColor) : undefined
|
|
93
|
+
if (!col) continue
|
|
94
|
+
|
|
95
|
+
//width
|
|
96
|
+
/** @type {number|undefined} */
|
|
97
|
+
const wG = this.width ? this.width(c[this.widthCol], r, statWidth, zf) : undefined
|
|
98
|
+
if (!wG || wG < 0) continue
|
|
99
|
+
|
|
100
|
+
//length
|
|
101
|
+
/** @type {number|undefined} */
|
|
102
|
+
const lG = this.length ? this.length(c[this.lengthCol], r, statLength, zf) : undefined
|
|
103
|
+
if (!lG || lG < 0) continue
|
|
104
|
+
|
|
105
|
+
//orientation (in radian)
|
|
106
|
+
/** @type {number} */
|
|
107
|
+
const or = this.orientation(c) * f
|
|
108
|
+
if (or === undefined || isNaN(or)) continue
|
|
109
|
+
|
|
110
|
+
//get offset
|
|
111
|
+
const offset = this.offset(c, r, zf)
|
|
112
|
+
|
|
113
|
+
//set color and width
|
|
114
|
+
cg.ctx.strokeStyle = col
|
|
115
|
+
cg.ctx.lineWidth = wG
|
|
116
|
+
|
|
117
|
+
//compute segment centre postition
|
|
118
|
+
const cx = c.x + r / 2 + offset.dx
|
|
119
|
+
const cy = c.y + r / 2 + offset.dy
|
|
120
|
+
|
|
121
|
+
//compute segment direction
|
|
122
|
+
const dx = 0.5 * Math.cos(or) * lG
|
|
123
|
+
const dy = 0.5 * Math.sin(or) * lG
|
|
124
|
+
|
|
125
|
+
//draw segment
|
|
126
|
+
cg.ctx.beginPath()
|
|
127
|
+
cg.ctx.moveTo(cx - dx, cy - dy)
|
|
128
|
+
cg.ctx.lineTo(cx + dx, cy + dy)
|
|
129
|
+
cg.ctx.stroke()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
//update legend, if any
|
|
133
|
+
this.updateLegends({
|
|
134
|
+
style: this,
|
|
135
|
+
r: r,
|
|
136
|
+
zf: zf,
|
|
137
|
+
sColor: statColor,
|
|
138
|
+
sLength: statLength,
|
|
139
|
+
sWidth: statWidth,
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
import { Style } from '../Style'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A very generic style that shows grid cells with specific color, size and shape.
|
|
8
|
+
* It can be used to show variables as cell colors, cell size, cell shape, or any combination of the three visual variables.
|
|
9
|
+
*
|
|
10
|
+
* @author Joseph Davies, Julien Gaffuri
|
|
11
|
+
*/
|
|
12
|
+
export class ShapeColorSizeStyle extends Style {
|
|
13
|
+
/** @param {object} opts */
|
|
14
|
+
constructor(opts) {
|
|
15
|
+
super(opts)
|
|
16
|
+
opts = opts || {}
|
|
17
|
+
|
|
18
|
+
/** The name of the column/attribute of the tabular data where to retrieve the variable for color.
|
|
19
|
+
* @type {string} */
|
|
20
|
+
this.colorCol = opts.colorCol
|
|
21
|
+
|
|
22
|
+
/** A function returning the color of the cell.
|
|
23
|
+
* @type {function(number,number,import("../Style").Stat|undefined,number):string} */
|
|
24
|
+
this.color = opts.color || (() => '#EA6BAC') //(v,r,s,zf) => {}
|
|
25
|
+
|
|
26
|
+
/** The name of the column/attribute of the tabular data where to retrieve the variable for size.
|
|
27
|
+
* @type {string} */
|
|
28
|
+
this.sizeCol = opts.sizeCol
|
|
29
|
+
|
|
30
|
+
/** A function returning the size of a cell in geographical unit.
|
|
31
|
+
* @type {function(number,number,import("../Style").Stat|undefined,number):number} */
|
|
32
|
+
this.size = opts.size
|
|
33
|
+
|
|
34
|
+
/** A function returning the shape of a cell.
|
|
35
|
+
* @type {function(import("../Dataset").Cell):import("../Style").Shape} */
|
|
36
|
+
this.shape = opts.shape || (() => 'square')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Draw cells as squares, with various colors and size.
|
|
41
|
+
*
|
|
42
|
+
* @param {Array.<import("../Dataset").Cell>} cells
|
|
43
|
+
* @param {number} r
|
|
44
|
+
* @param {import("../GeoCanvas").GeoCanvas} cg
|
|
45
|
+
*/
|
|
46
|
+
draw(cells, r, cg) {
|
|
47
|
+
//filter
|
|
48
|
+
if (this.filter) cells = cells.filter(this.filter)
|
|
49
|
+
|
|
50
|
+
//zoom factor
|
|
51
|
+
const zf = cg.getZf()
|
|
52
|
+
|
|
53
|
+
let statSize
|
|
54
|
+
if (this.sizeCol) {
|
|
55
|
+
//if size is used, sort cells by size so that the biggest are drawn first
|
|
56
|
+
cells.sort((c1, c2) => c2[this.sizeCol] - c1[this.sizeCol])
|
|
57
|
+
//and compute size variable statistics
|
|
58
|
+
statSize = Style.getStatistics(cells, (c) => c[this.sizeCol], true)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let statColor
|
|
62
|
+
if (this.colorCol) {
|
|
63
|
+
//compute color variable statistics
|
|
64
|
+
statColor = Style.getStatistics(cells, (c) => c[this.colorCol], true)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
//draw with HTML canvas
|
|
68
|
+
//in geo coordinates
|
|
69
|
+
cg.setCanvasTransform()
|
|
70
|
+
|
|
71
|
+
const r2 = r * 0.5
|
|
72
|
+
for (let cell of cells) {
|
|
73
|
+
//color
|
|
74
|
+
const col = this.color ? this.color(cell[this.colorCol], r, statColor, zf) : undefined
|
|
75
|
+
if (!col || col === 'none') continue
|
|
76
|
+
cg.ctx.fillStyle = col
|
|
77
|
+
|
|
78
|
+
//shape
|
|
79
|
+
const shape = this.shape ? this.shape(cell) : 'square'
|
|
80
|
+
if (shape === 'none') continue
|
|
81
|
+
|
|
82
|
+
//size
|
|
83
|
+
/** @type {function(number,number,import("../Style").Stat|undefined,number):number} */
|
|
84
|
+
let s_ = this.size || (() => r)
|
|
85
|
+
//size - in geo unit
|
|
86
|
+
const sG = s_(cell[this.sizeCol], r, statSize, zf)
|
|
87
|
+
|
|
88
|
+
//get offset
|
|
89
|
+
const offset = this.offset(cell, r, zf)
|
|
90
|
+
|
|
91
|
+
if (shape === 'square') {
|
|
92
|
+
//draw square
|
|
93
|
+
const d = r * (1 - sG / r) * 0.5
|
|
94
|
+
cg.ctx.fillRect(cell.x + d + offset.dx, cell.y + d + offset.dy, sG, sG)
|
|
95
|
+
} else if (shape === 'circle') {
|
|
96
|
+
//draw circle
|
|
97
|
+
cg.ctx.beginPath()
|
|
98
|
+
cg.ctx.arc(cell.x + r2 + offset.dx, cell.y + r2 + offset.dy, sG * 0.5, 0, 2 * Math.PI, false)
|
|
99
|
+
cg.ctx.fill()
|
|
100
|
+
} else if (shape === 'donut') {
|
|
101
|
+
//draw donut
|
|
102
|
+
const xc = cell.x + r2 + offset.dx,
|
|
103
|
+
yc = cell.y + r2 + offset.dy
|
|
104
|
+
cg.ctx.beginPath()
|
|
105
|
+
cg.ctx.moveTo(xc, yc)
|
|
106
|
+
cg.ctx.arc(xc, yc, r2, 0, 2 * Math.PI)
|
|
107
|
+
cg.ctx.arc(xc, yc, (1 - sG / r) * r2, 0, 2 * Math.PI, true)
|
|
108
|
+
cg.ctx.closePath()
|
|
109
|
+
cg.ctx.fill()
|
|
110
|
+
} else if (shape === 'diamond') {
|
|
111
|
+
const s2 = sG * 0.5
|
|
112
|
+
cg.ctx.beginPath()
|
|
113
|
+
cg.ctx.moveTo(cell.x + r2 - s2, cell.y + r2)
|
|
114
|
+
cg.ctx.lineTo(cell.x + r2, cell.y + r2 + s2)
|
|
115
|
+
cg.ctx.lineTo(cell.x + r2 + s2, cell.y + r2)
|
|
116
|
+
cg.ctx.lineTo(cell.x + r2, cell.y + r2 - s2)
|
|
117
|
+
cg.ctx.fill()
|
|
118
|
+
} else {
|
|
119
|
+
throw new Error('Unexpected shape:' + shape)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
//update legends
|
|
124
|
+
this.updateLegends({ style: this, r: r, zf: zf, sSize: statSize, sColor: statColor })
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
import { Style } from '../Style'
|
|
5
|
+
|
|
6
|
+
/** @typedef {{x:number,y:number,or:"v"|"h",v1:string|undefined,v2:string|undefined}} Side */
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A style to show the sides of grid cells based on their different categories.
|
|
10
|
+
*
|
|
11
|
+
* @author Julien Gaffuri
|
|
12
|
+
*/
|
|
13
|
+
export class SideCatStyle extends Style {
|
|
14
|
+
/** @param {object} opts */
|
|
15
|
+
constructor(opts) {
|
|
16
|
+
super(opts)
|
|
17
|
+
opts = opts || {}
|
|
18
|
+
|
|
19
|
+
/** The name of the column/attribute of the tabular data where to retrieve the categorical value.
|
|
20
|
+
* @type {string} */
|
|
21
|
+
this.col = opts.col
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The dictionary (string -> color) which give the color of each category.
|
|
25
|
+
* @type {object} */
|
|
26
|
+
this.color = opts.color
|
|
27
|
+
|
|
28
|
+
/** A function returning the width of a cell side line, in geo unit
|
|
29
|
+
* @type {function(Side,number,number):number} */
|
|
30
|
+
this.width = opts.width || ((side, r, z) => r * 0.2)
|
|
31
|
+
|
|
32
|
+
/** A fill color for the cells.
|
|
33
|
+
* @type {function(import("../Dataset").Cell):string} */
|
|
34
|
+
this.fillColor = opts.fillColor
|
|
35
|
+
}
|
|
36
|
+
|
|
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
|
+
if (!cells || cells.length == 0) return
|
|
47
|
+
|
|
48
|
+
//zoom factor
|
|
49
|
+
const zf = cg.getZf()
|
|
50
|
+
|
|
51
|
+
/** @type {Array.<Side>} */
|
|
52
|
+
const sides = []
|
|
53
|
+
|
|
54
|
+
//make horizontal sides
|
|
55
|
+
//sort cells by x and y
|
|
56
|
+
cells.sort((c1, c2) => (c2.x == c1.x ? c1.y - c2.y : c1.x - c2.x))
|
|
57
|
+
let c1 = cells[0]
|
|
58
|
+
let v1 = c1[this.col]
|
|
59
|
+
for (let i = 1; i < cells.length; i++) {
|
|
60
|
+
let c2 = cells[i]
|
|
61
|
+
let v2 = c2[this.col]
|
|
62
|
+
|
|
63
|
+
if (c1.y + r == c2.y && c1.x == c2.x) {
|
|
64
|
+
//cells in same column and touch along horizontal side
|
|
65
|
+
//make shared side
|
|
66
|
+
if (v1 != v2) sides.push({ x: c1.x, y: c2.y, or: 'h', v1: v1, v2: v2 })
|
|
67
|
+
} else {
|
|
68
|
+
//cells do not touch along horizontal side
|
|
69
|
+
//make two sides: top one for c1, bottom for c2
|
|
70
|
+
sides.push({ x: c1.x, y: c1.y + r, or: 'h', v1: v1, v2: undefined })
|
|
71
|
+
sides.push({ x: c2.x, y: c2.y, or: 'h', v1: undefined, v2: v2 })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
c1 = c2
|
|
75
|
+
v1 = v2
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
//make vertical sides
|
|
79
|
+
//sort cells by y and x
|
|
80
|
+
cells.sort((c1, c2) => (c2.y == c1.y ? c1.x - c2.x : c1.y - c2.y))
|
|
81
|
+
c1 = cells[0]
|
|
82
|
+
v1 = c1[this.col]
|
|
83
|
+
for (let i = 1; i < cells.length; i++) {
|
|
84
|
+
let c2 = cells[i]
|
|
85
|
+
let v2 = c2[this.col]
|
|
86
|
+
|
|
87
|
+
if (c1.x + r == c2.x && c1.y == c2.y) {
|
|
88
|
+
//cells in same row and touch along vertical side
|
|
89
|
+
//make shared side
|
|
90
|
+
if (v1 != v2) sides.push({ x: c1.x + r, y: c1.y, or: 'v', v1: v1, v2: v2 })
|
|
91
|
+
} else {
|
|
92
|
+
//cells do not touch along vertical side
|
|
93
|
+
//make two sides: right one for c1, left for c2
|
|
94
|
+
sides.push({ x: c1.x + r, y: c1.y, or: 'v', v1: v1, v2: undefined })
|
|
95
|
+
sides.push({ x: c2.x, y: c2.y, or: 'v', v1: undefined, v2: v2 })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
c1 = c2
|
|
99
|
+
v1 = v2
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
//
|
|
103
|
+
if (sides.length == 0) return
|
|
104
|
+
|
|
105
|
+
//draw in geo coordinates
|
|
106
|
+
cg.setCanvasTransform()
|
|
107
|
+
|
|
108
|
+
//draw cells, if fillColor specified
|
|
109
|
+
if (this.fillColor)
|
|
110
|
+
for (let c of cells) {
|
|
111
|
+
const fc = this.fillColor(c)
|
|
112
|
+
if (!fc || fc == 'none') continue
|
|
113
|
+
cg.ctx.fillStyle = fc
|
|
114
|
+
cg.ctx.fillRect(c.x, c.y, r, r)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
//draw sides
|
|
118
|
+
cg.ctx.lineCap = 'butt'
|
|
119
|
+
for (let s of sides) {
|
|
120
|
+
//width
|
|
121
|
+
/** @type {number|undefined} */
|
|
122
|
+
const wG = this.width ? this.width(s, r, zf) : undefined
|
|
123
|
+
if (!wG || wG <= 0) continue
|
|
124
|
+
const w2 = wG * 0.5
|
|
125
|
+
|
|
126
|
+
//set color and width
|
|
127
|
+
cg.ctx.lineWidth = wG
|
|
128
|
+
|
|
129
|
+
//draw segment with correct orientation
|
|
130
|
+
if (s.or === 'h') {
|
|
131
|
+
//top line
|
|
132
|
+
if (s.v2) {
|
|
133
|
+
cg.ctx.beginPath()
|
|
134
|
+
cg.ctx.strokeStyle = this.color[s.v2]
|
|
135
|
+
cg.ctx.moveTo(s.x, s.y + w2)
|
|
136
|
+
cg.ctx.lineTo(s.x + r, s.y + w2)
|
|
137
|
+
cg.ctx.stroke()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
//bottom line
|
|
141
|
+
if (s.v1) {
|
|
142
|
+
cg.ctx.beginPath()
|
|
143
|
+
cg.ctx.strokeStyle = this.color[s.v1]
|
|
144
|
+
cg.ctx.moveTo(s.x, s.y - w2)
|
|
145
|
+
cg.ctx.lineTo(s.x + r, s.y - w2)
|
|
146
|
+
cg.ctx.stroke()
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
//right line
|
|
150
|
+
if (s.v2) {
|
|
151
|
+
cg.ctx.beginPath()
|
|
152
|
+
cg.ctx.strokeStyle = this.color[s.v2]
|
|
153
|
+
cg.ctx.moveTo(s.x + w2, s.y)
|
|
154
|
+
cg.ctx.lineTo(s.x + w2, s.y + r)
|
|
155
|
+
cg.ctx.stroke()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
//left line
|
|
159
|
+
if (s.v1) {
|
|
160
|
+
cg.ctx.beginPath()
|
|
161
|
+
cg.ctx.strokeStyle = this.color[s.v1]
|
|
162
|
+
cg.ctx.moveTo(s.x - w2, s.y)
|
|
163
|
+
cg.ctx.lineTo(s.x - w2, s.y + r)
|
|
164
|
+
cg.ctx.stroke()
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
//update legends
|
|
170
|
+
this.updateLegends({ style: this, r: r, zf: zf })
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
import { Style } from '../Style'
|
|
5
|
+
|
|
6
|
+
/** @typedef {{x:number,y:number,or:"v"|"h",value:number}} Side */
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
*
|
|
10
|
+
* @author Julien Gaffuri
|
|
11
|
+
*/
|
|
12
|
+
export class SideStyle extends Style {
|
|
13
|
+
/** @param {object} opts */
|
|
14
|
+
constructor(opts) {
|
|
15
|
+
super(opts)
|
|
16
|
+
opts = opts || {}
|
|
17
|
+
|
|
18
|
+
/** The name of the column/attribute of the tabular data where to retrieve the variable for the cell values.
|
|
19
|
+
* @type {string} */
|
|
20
|
+
this.valueCol = opts.valueCol
|
|
21
|
+
|
|
22
|
+
/** A function returning the value of a cell side. This value is computed from the two adjacent cell values.
|
|
23
|
+
* For horizontal sides, v1 is the value of the cell below and v2 the value of the cell above.
|
|
24
|
+
* For vertical sides, v1 is the value of the cell left and v2 the value of the cell right.
|
|
25
|
+
* @type {function(number|undefined,number|undefined,number,import("../Style").Stat|undefined,number):number} */
|
|
26
|
+
this.value = opts.value || ((v1, v2, r, s, zf) => 1)
|
|
27
|
+
|
|
28
|
+
/** A function returning the color of a cell side.
|
|
29
|
+
* @type {function(Side,number,import("../Style").Stat|undefined,number):string} */
|
|
30
|
+
this.color = opts.color || (() => '#EA6BAC')
|
|
31
|
+
|
|
32
|
+
/** A function returning the width of a cell side, in geo unit
|
|
33
|
+
* @type {function(Side,number,import("../Style").Stat|undefined,number):number} */
|
|
34
|
+
this.width = opts.width || ((side, r, s, zf) => (r * side.value) / 5)
|
|
35
|
+
|
|
36
|
+
/** orientation. Set to 90 to show sides as slope lines for example.
|
|
37
|
+
* @type {number} */
|
|
38
|
+
this.orientation = opts.orientation || 0
|
|
39
|
+
|
|
40
|
+
/** A fill color for the cells.
|
|
41
|
+
* @type {function(import("../Dataset").Cell):string} */
|
|
42
|
+
this.fillColor = opts.fillColor
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {Array.<import("../Dataset").Cell>} cells
|
|
47
|
+
* @param {number} r
|
|
48
|
+
* @param {import("../GeoCanvas").GeoCanvas} cg
|
|
49
|
+
*/
|
|
50
|
+
draw(cells, r, cg) {
|
|
51
|
+
//filter
|
|
52
|
+
if (this.filter) cells = cells.filter(this.filter)
|
|
53
|
+
|
|
54
|
+
//zoom factor
|
|
55
|
+
const zf = cg.getZf()
|
|
56
|
+
|
|
57
|
+
//compute stats on cell values
|
|
58
|
+
let statValue
|
|
59
|
+
if (this.valueCol) {
|
|
60
|
+
//compute color variable statistics
|
|
61
|
+
statValue = Style.getStatistics(cells, (c) => c[this.valueCol], true)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** @type {Array.<Side>} */
|
|
65
|
+
const sides = []
|
|
66
|
+
|
|
67
|
+
const epsilon = r * 0.001
|
|
68
|
+
|
|
69
|
+
//make horizontal sides
|
|
70
|
+
//sort cells by x and y
|
|
71
|
+
cells.sort((c1, c2) => (c2.x == c1.x ? c1.y - c2.y : c1.x - c2.x))
|
|
72
|
+
let c1 = cells[0]
|
|
73
|
+
for (let i = 1; i < cells.length; i++) {
|
|
74
|
+
let c2 = cells[i]
|
|
75
|
+
|
|
76
|
+
if (Math.abs(c1.y + r - c2.y) <= epsilon && Math.abs(c1.x - c2.x) <= epsilon)
|
|
77
|
+
//cells in same column and touch along horizontal side
|
|
78
|
+
//make shared side
|
|
79
|
+
sides.push({
|
|
80
|
+
x: c1.x,
|
|
81
|
+
y: c2.y,
|
|
82
|
+
or: 'h',
|
|
83
|
+
value: this.value(c1[this.valueCol], c2[this.valueCol], r, statValue, zf),
|
|
84
|
+
})
|
|
85
|
+
else {
|
|
86
|
+
//cells do not touch along horizontal side
|
|
87
|
+
//make two sides: top one for c1, bottom for c2
|
|
88
|
+
sides.push({
|
|
89
|
+
x: c1.x,
|
|
90
|
+
y: c1.y + r,
|
|
91
|
+
or: 'h',
|
|
92
|
+
value: this.value(c1[this.valueCol], undefined, r, statValue, zf),
|
|
93
|
+
})
|
|
94
|
+
sides.push({
|
|
95
|
+
x: c2.x,
|
|
96
|
+
y: c2.y,
|
|
97
|
+
or: 'h',
|
|
98
|
+
value: this.value(undefined, c2[this.valueCol], r, statValue, zf),
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
c1 = c2
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
//make vertical sides
|
|
106
|
+
//sort cells by y and x
|
|
107
|
+
cells.sort((c1, c2) => (c2.y == c1.y ? c1.x - c2.x : c1.y - c2.y))
|
|
108
|
+
c1 = cells[0]
|
|
109
|
+
for (let i = 1; i < cells.length; i++) {
|
|
110
|
+
let c2 = cells[i]
|
|
111
|
+
|
|
112
|
+
if (Math.abs(c1.x + r - c2.x) <= epsilon && Math.abs(c1.y - c2.y) <= epsilon)
|
|
113
|
+
//cells in same row and touch along vertical side
|
|
114
|
+
//make shared side
|
|
115
|
+
sides.push({
|
|
116
|
+
x: c1.x + r,
|
|
117
|
+
y: c1.y,
|
|
118
|
+
or: 'v',
|
|
119
|
+
value: this.value(c1[this.valueCol], c2[this.valueCol], r, statValue, zf),
|
|
120
|
+
})
|
|
121
|
+
else {
|
|
122
|
+
//cells do not touch along vertical side
|
|
123
|
+
//make two sides: right one for c1, left for c2
|
|
124
|
+
sides.push({
|
|
125
|
+
x: c1.x + r,
|
|
126
|
+
y: c1.y,
|
|
127
|
+
or: 'v',
|
|
128
|
+
value: this.value(c1[this.valueCol], undefined, r, statValue, zf),
|
|
129
|
+
})
|
|
130
|
+
sides.push({
|
|
131
|
+
x: c2.x,
|
|
132
|
+
y: c2.y,
|
|
133
|
+
or: 'v',
|
|
134
|
+
value: this.value(undefined, c2[this.valueCol], r, statValue, zf),
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
c1 = c2
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
//
|
|
142
|
+
if (sides.length == 0) return
|
|
143
|
+
|
|
144
|
+
//compute stats on sides
|
|
145
|
+
const statSides = SideStyle.getSideStatistics(sides, true)
|
|
146
|
+
|
|
147
|
+
//draw in geo coordinates
|
|
148
|
+
cg.setCanvasTransform()
|
|
149
|
+
|
|
150
|
+
//draw cells, if fillColor specified
|
|
151
|
+
if (this.fillColor)
|
|
152
|
+
for (let c of cells) {
|
|
153
|
+
const fc = this.fillColor(c)
|
|
154
|
+
if (!fc || fc == 'none') continue
|
|
155
|
+
cg.ctx.fillStyle = fc
|
|
156
|
+
cg.ctx.fillRect(c.x, c.y, r, r)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
//draw sides
|
|
160
|
+
cg.ctx.lineCap = 'butt'
|
|
161
|
+
const r2 = r / 2
|
|
162
|
+
for (let s of sides) {
|
|
163
|
+
//color
|
|
164
|
+
/** @type {string|undefined} */
|
|
165
|
+
const col = this.color ? this.color(s, r, statSides, zf) : undefined
|
|
166
|
+
if (!col || col == 'none') continue
|
|
167
|
+
|
|
168
|
+
//width
|
|
169
|
+
/** @type {number|undefined} */
|
|
170
|
+
const wG = this.width ? this.width(s, r, statSides, zf) : undefined
|
|
171
|
+
if (!wG || wG <= 0) continue
|
|
172
|
+
|
|
173
|
+
//set color and width
|
|
174
|
+
cg.ctx.strokeStyle = col
|
|
175
|
+
cg.ctx.lineWidth = wG
|
|
176
|
+
|
|
177
|
+
//draw segment with correct orientation
|
|
178
|
+
cg.ctx.beginPath()
|
|
179
|
+
if (this.orientation == 90) {
|
|
180
|
+
cg.ctx.moveTo(s.x + r2, s.y + r2)
|
|
181
|
+
if (s.or === 'h') cg.ctx.lineTo(s.x + r2, s.y - r2)
|
|
182
|
+
else cg.ctx.lineTo(s.x - r2, s.y + r2)
|
|
183
|
+
} else {
|
|
184
|
+
cg.ctx.moveTo(s.x, s.y)
|
|
185
|
+
cg.ctx.lineTo(s.x + (s.or === 'h' ? r : 0), s.y + (s.or === 'v' ? r : 0))
|
|
186
|
+
}
|
|
187
|
+
cg.ctx.stroke()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
//update legends
|
|
191
|
+
this.updateLegends({ style: this, r: r, zf: zf })
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Compute some statistics on a value of some sides.
|
|
196
|
+
* This is used to define how to draw specifically the sides within the view.
|
|
197
|
+
*
|
|
198
|
+
* @param {Array.<Side>} sides
|
|
199
|
+
* @param {boolean} ignoreZeros
|
|
200
|
+
* @returns {import("../Style").Stat | undefined}
|
|
201
|
+
*/
|
|
202
|
+
static getSideStatistics(sides, ignoreZeros) {
|
|
203
|
+
if (!sides || sides.length == 0) return undefined
|
|
204
|
+
let min = Infinity
|
|
205
|
+
let max = -Infinity
|
|
206
|
+
//let sum = 0
|
|
207
|
+
//let nb = 0
|
|
208
|
+
for (const s of sides) {
|
|
209
|
+
const v = s.value
|
|
210
|
+
if (ignoreZeros && !v) continue
|
|
211
|
+
if (v < min) min = v
|
|
212
|
+
if (v > max) max = v
|
|
213
|
+
//sum += v
|
|
214
|
+
//nb++
|
|
215
|
+
}
|
|
216
|
+
return { min: min, max: max }
|
|
217
|
+
}
|
|
218
|
+
}
|