gridviz 3.0.23 → 3.0.24
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/README.md +128 -128
- package/dist/gridviz.js +8618 -8579
- package/dist/gridviz.min.js +1 -1
- package/package.json +74 -74
- package/src/button/Button.js +67 -67
- package/src/button/FullscreenButton.js +121 -121
- package/src/button/ZoomButtons.js +103 -101
- package/src/core/Dataset.js +154 -154
- package/src/core/Drawable.js +78 -78
- package/src/core/GeoCanvas.js +437 -430
- package/src/core/Layer.js +23 -23
- package/src/core/Legend.js +108 -108
- package/src/core/Map.js +542 -516
- package/src/core/MultiResolutionDataset.js +93 -93
- package/src/core/Style.js +98 -98
- package/src/core/Tooltip.js +182 -182
- package/src/dataset/COGeo.js +25 -25
- package/src/dataset/CSVGrid.js +112 -112
- package/src/dataset/GeoTIFF.js +122 -122
- package/src/dataset/JSGrid.js +55 -55
- package/src/dataset/TiledGrid.js +302 -302
- package/src/dataset/worker.js +18 -18
- package/src/index.js +77 -77
- package/src/layer/BackgroundLayer.js +173 -173
- package/src/layer/BackgroundLayerImage.js +81 -81
- package/src/layer/BackgroundLayerWMS.js +104 -104
- package/src/layer/GeoJSONLayer.js +214 -214
- package/src/layer/GridLayer.js +148 -148
- package/src/layer/LabelLayer.js +194 -194
- package/src/legend/ColorCategoryLegend.js +109 -109
- package/src/legend/ColorDiscreteLegend.js +100 -100
- package/src/legend/ColorLegend.js +115 -115
- package/src/legend/OrientationLegend.js +89 -89
- package/src/legend/SizeLegend.js +224 -224
- package/src/legend/TernaryLegend.js +302 -302
- package/src/style/CompositionStyle.js +291 -291
- package/src/style/ContourStyle.js +21 -21
- package/src/style/DotDensityStyle.js +133 -133
- package/src/style/ImageStyle.js +100 -100
- package/src/style/IsoFenceStyle.js +217 -217
- package/src/style/JoyPlotStyle.js +127 -127
- package/src/style/LegoStyle.js +110 -110
- package/src/style/MosaicStyle.js +99 -99
- package/src/style/NinjaStarStyle.js +102 -102
- package/src/style/PillarStyle.js +207 -207
- package/src/style/SegmentStyle.js +105 -105
- package/src/style/ShapeColorSizeStyle.js +147 -147
- package/src/style/SideCategoryStyle.js +112 -112
- package/src/style/SideStyle.js +212 -212
- package/src/style/SquareColorCategoryWebGLStyle.js +107 -107
- package/src/style/SquareColorWebGLStyle.js +126 -126
- package/src/style/StrokeStyle.js +135 -135
- package/src/style/TanakaStyle.js +69 -69
- package/src/style/TextStyle.js +110 -110
- package/src/style/TimeSeriesStyle.js +222 -222
- package/src/utils/WebGLRectangleColoring.js +106 -106
- package/src/utils/WebGLSquareColoring.js +118 -118
- package/src/utils/WebGLSquareColoringAdvanced.js +230 -230
- package/src/utils/WebGLSquareColoringCatAdvanced.js +133 -133
- package/src/utils/WebGLSquareColoringKS.js +153 -153
- package/src/utils/scale.js +218 -218
- package/src/utils/stretching.js +120 -120
- package/src/utils/ternary.js +208 -208
- package/src/utils/utils.js +58 -58
- package/src/utils/webGLUtils.js +72 -72
- package/src/utils/zoomUtils.js +142 -142
package/src/style/TextStyle.js
CHANGED
|
@@ -1,110 +1,110 @@
|
|
|
1
|
-
//@ts-check
|
|
2
|
-
'use strict'
|
|
3
|
-
|
|
4
|
-
import { Style } from '../core/Style.js'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @module style
|
|
8
|
-
* @author Julien Gaffuri
|
|
9
|
-
*/
|
|
10
|
-
export class TextStyle extends Style {
|
|
11
|
-
/** @param {object} opts */
|
|
12
|
-
constructor(opts) {
|
|
13
|
-
super(opts)
|
|
14
|
-
opts = opts || {}
|
|
15
|
-
|
|
16
|
-
/** A function returning the text of a cell.
|
|
17
|
-
* @type {function(import('../core/Dataset.js').Cell, number, number, object):string} */
|
|
18
|
-
this.text = opts.text || (() => 'X') //(c,r,z,vs) => {}
|
|
19
|
-
|
|
20
|
-
/** A function returning the color of the cell.
|
|
21
|
-
* @type {function(import('../core/Dataset.js').Cell, number, number, object):string} */
|
|
22
|
-
this.color = opts.color || (() => 'black') //(c,r,z,vs) => {}
|
|
23
|
-
|
|
24
|
-
/** A function returning the font size of a cell in geo unit.
|
|
25
|
-
* @type {function(import('../core/Dataset.js').Cell, number, number,object):number} */
|
|
26
|
-
this.fontSize = opts.fontSize || ((cell, resolution) => resolution) //(c,r,z,vs) => {}
|
|
27
|
-
|
|
28
|
-
/** The text font family.
|
|
29
|
-
* @type {function(import('../core/Dataset.js').Cell, number, number, object):string} */
|
|
30
|
-
this.fontFamily = opts.fontFamily || (() => 'Arial')
|
|
31
|
-
|
|
32
|
-
/** The text font weight.
|
|
33
|
-
* @type {function(import('../core/Dataset.js').Cell, number, number, object):string} */
|
|
34
|
-
this.fontWeight = opts.fontWeight || (() => 'bold')
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Draw cells as text.
|
|
39
|
-
*
|
|
40
|
-
* @param {Array.<import("../core/Dataset").Cell>} cells
|
|
41
|
-
* @param {import("../core/GeoCanvas").GeoCanvas} geoCanvas
|
|
42
|
-
* @param {number} resolution
|
|
43
|
-
*/
|
|
44
|
-
draw(cells, geoCanvas, resolution) {
|
|
45
|
-
//filter
|
|
46
|
-
if (this.filter) cells = cells.filter(this.filter)
|
|
47
|
-
|
|
48
|
-
//
|
|
49
|
-
const z = geoCanvas.view.z
|
|
50
|
-
const ctx = geoCanvas.offscreenCtx
|
|
51
|
-
//get view scale
|
|
52
|
-
const viewScale = this.viewScale ? this.viewScale(cells, resolution, z) : undefined
|
|
53
|
-
|
|
54
|
-
//draw with HTML canvas
|
|
55
|
-
//in screen coordinates
|
|
56
|
-
geoCanvas.initCanvasTransform()
|
|
57
|
-
|
|
58
|
-
for (let cell of cells) {
|
|
59
|
-
//get cell text
|
|
60
|
-
const text = this.text ? this.text(cell, resolution, z, viewScale) : undefined
|
|
61
|
-
if (text == undefined || text == null || text + '' === '') continue
|
|
62
|
-
|
|
63
|
-
//color
|
|
64
|
-
const col = this.color ? this.color(cell, resolution, z, viewScale) : undefined
|
|
65
|
-
if (!col) continue
|
|
66
|
-
ctx.fillStyle = col
|
|
67
|
-
|
|
68
|
-
//font size
|
|
69
|
-
//size - in pixel unit
|
|
70
|
-
const fontSizePix = this.fontSize(cell, resolution, z, viewScale) / z
|
|
71
|
-
if (!fontSizePix) continue
|
|
72
|
-
|
|
73
|
-
//set font
|
|
74
|
-
const fontFamily = this.fontFamily ? this.fontFamily(cell, resolution, z, viewScale) : 'Arial'
|
|
75
|
-
const fontWeight = this.fontWeight ? this.fontWeight(cell, resolution, z, viewScale) : 'bold'
|
|
76
|
-
ctx.font = fontWeight + ' ' + fontSizePix + 'px ' + fontFamily
|
|
77
|
-
|
|
78
|
-
//get offset
|
|
79
|
-
const offset = this.offset(cell, resolution, z)
|
|
80
|
-
|
|
81
|
-
//text position
|
|
82
|
-
ctx.textAlign = 'center'
|
|
83
|
-
const tx = geoCanvas.geoToPixX(cell.x + resolution * 0.5 + offset.dx)
|
|
84
|
-
const ty = geoCanvas.geoToPixY(cell.y + resolution * 0.5 + offset.dy) + fontSizePix * 0.3 //it should be 0.5 but 0.3 seems to work better
|
|
85
|
-
|
|
86
|
-
//draw the text
|
|
87
|
-
ctx.fillText(text, tx, ty)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
//update legends
|
|
91
|
-
this.updateLegends({ style: this, resolution: resolution, z: z, viewScale: viewScale })
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Build a function [0,1]->string for characters legend
|
|
96
|
-
*
|
|
97
|
-
* @param {Array.<string>} chars
|
|
98
|
-
* @param {(function(number):number)|undefined} scale
|
|
99
|
-
* @returns {function(number):string}
|
|
100
|
-
*/
|
|
101
|
-
static textScale(chars, scale = undefined) {
|
|
102
|
-
const nb = chars.length
|
|
103
|
-
return (t) => {
|
|
104
|
-
if (scale) t = scale(t)
|
|
105
|
-
if (t == 0) return ''
|
|
106
|
-
if (t >= 1) return chars[nb - 1]
|
|
107
|
-
return chars[Math.floor(t * nb)]
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
1
|
+
//@ts-check
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
import { Style } from '../core/Style.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @module style
|
|
8
|
+
* @author Julien Gaffuri
|
|
9
|
+
*/
|
|
10
|
+
export class TextStyle extends Style {
|
|
11
|
+
/** @param {object} opts */
|
|
12
|
+
constructor(opts) {
|
|
13
|
+
super(opts)
|
|
14
|
+
opts = opts || {}
|
|
15
|
+
|
|
16
|
+
/** A function returning the text of a cell.
|
|
17
|
+
* @type {function(import('../core/Dataset.js').Cell, number, number, object):string} */
|
|
18
|
+
this.text = opts.text || (() => 'X') //(c,r,z,vs) => {}
|
|
19
|
+
|
|
20
|
+
/** A function returning the color of the cell.
|
|
21
|
+
* @type {function(import('../core/Dataset.js').Cell, number, number, object):string} */
|
|
22
|
+
this.color = opts.color || (() => 'black') //(c,r,z,vs) => {}
|
|
23
|
+
|
|
24
|
+
/** A function returning the font size of a cell in geo unit.
|
|
25
|
+
* @type {function(import('../core/Dataset.js').Cell, number, number,object):number} */
|
|
26
|
+
this.fontSize = opts.fontSize || ((cell, resolution) => resolution) //(c,r,z,vs) => {}
|
|
27
|
+
|
|
28
|
+
/** The text font family.
|
|
29
|
+
* @type {function(import('../core/Dataset.js').Cell, number, number, object):string} */
|
|
30
|
+
this.fontFamily = opts.fontFamily || (() => 'Arial')
|
|
31
|
+
|
|
32
|
+
/** The text font weight.
|
|
33
|
+
* @type {function(import('../core/Dataset.js').Cell, number, number, object):string} */
|
|
34
|
+
this.fontWeight = opts.fontWeight || (() => 'bold')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Draw cells as text.
|
|
39
|
+
*
|
|
40
|
+
* @param {Array.<import("../core/Dataset").Cell>} cells
|
|
41
|
+
* @param {import("../core/GeoCanvas").GeoCanvas} geoCanvas
|
|
42
|
+
* @param {number} resolution
|
|
43
|
+
*/
|
|
44
|
+
draw(cells, geoCanvas, resolution) {
|
|
45
|
+
//filter
|
|
46
|
+
if (this.filter) cells = cells.filter(this.filter)
|
|
47
|
+
|
|
48
|
+
//
|
|
49
|
+
const z = geoCanvas.view.z
|
|
50
|
+
const ctx = geoCanvas.offscreenCtx
|
|
51
|
+
//get view scale
|
|
52
|
+
const viewScale = this.viewScale ? this.viewScale(cells, resolution, z) : undefined
|
|
53
|
+
|
|
54
|
+
//draw with HTML canvas
|
|
55
|
+
//in screen coordinates
|
|
56
|
+
geoCanvas.initCanvasTransform()
|
|
57
|
+
|
|
58
|
+
for (let cell of cells) {
|
|
59
|
+
//get cell text
|
|
60
|
+
const text = this.text ? this.text(cell, resolution, z, viewScale) : undefined
|
|
61
|
+
if (text == undefined || text == null || text + '' === '') continue
|
|
62
|
+
|
|
63
|
+
//color
|
|
64
|
+
const col = this.color ? this.color(cell, resolution, z, viewScale) : undefined
|
|
65
|
+
if (!col) continue
|
|
66
|
+
ctx.fillStyle = col
|
|
67
|
+
|
|
68
|
+
//font size
|
|
69
|
+
//size - in pixel unit
|
|
70
|
+
const fontSizePix = this.fontSize(cell, resolution, z, viewScale) / z
|
|
71
|
+
if (!fontSizePix) continue
|
|
72
|
+
|
|
73
|
+
//set font
|
|
74
|
+
const fontFamily = this.fontFamily ? this.fontFamily(cell, resolution, z, viewScale) : 'Arial'
|
|
75
|
+
const fontWeight = this.fontWeight ? this.fontWeight(cell, resolution, z, viewScale) : 'bold'
|
|
76
|
+
ctx.font = fontWeight + ' ' + fontSizePix + 'px ' + fontFamily
|
|
77
|
+
|
|
78
|
+
//get offset
|
|
79
|
+
const offset = this.offset(cell, resolution, z)
|
|
80
|
+
|
|
81
|
+
//text position
|
|
82
|
+
ctx.textAlign = 'center'
|
|
83
|
+
const tx = geoCanvas.geoToPixX(cell.x + resolution * 0.5 + offset.dx)
|
|
84
|
+
const ty = geoCanvas.geoToPixY(cell.y + resolution * 0.5 + offset.dy) + fontSizePix * 0.3 //it should be 0.5 but 0.3 seems to work better
|
|
85
|
+
|
|
86
|
+
//draw the text
|
|
87
|
+
ctx.fillText(text, tx, ty)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
//update legends
|
|
91
|
+
this.updateLegends({ style: this, resolution: resolution, z: z, viewScale: viewScale })
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Build a function [0,1]->string for characters legend
|
|
96
|
+
*
|
|
97
|
+
* @param {Array.<string>} chars
|
|
98
|
+
* @param {(function(number):number)|undefined} scale
|
|
99
|
+
* @returns {function(number):string}
|
|
100
|
+
*/
|
|
101
|
+
static textScale(chars, scale = undefined) {
|
|
102
|
+
const nb = chars.length
|
|
103
|
+
return (t) => {
|
|
104
|
+
if (scale) t = scale(t)
|
|
105
|
+
if (t == 0) return ''
|
|
106
|
+
if (t >= 1) return chars[nb - 1]
|
|
107
|
+
return chars[Math.floor(t * nb)]
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -1,222 +1,222 @@
|
|
|
1
|
-
//@ts-check
|
|
2
|
-
'use strict'
|
|
3
|
-
|
|
4
|
-
import { Style } from '../core/Style.js'
|
|
5
|
-
|
|
6
|
-
/** @typedef {"first"|"bottom"|"center"|"top"|"last"} AnchorModeYEnum */
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Show cell as timeseries chart
|
|
10
|
-
* Can be used for sparkline map of https://datagistips.hypotheses.org/488
|
|
11
|
-
*
|
|
12
|
-
* @module style
|
|
13
|
-
* @author Julien Gaffuri
|
|
14
|
-
*/
|
|
15
|
-
export class TimeSeriesStyle extends Style {
|
|
16
|
-
/** @param {object} opts */
|
|
17
|
-
constructor(opts) {
|
|
18
|
-
super(opts)
|
|
19
|
-
opts = opts || {}
|
|
20
|
-
|
|
21
|
-
/** The columns of the time series, ordered in chronological order.
|
|
22
|
-
* @type {Array.<string>} */
|
|
23
|
-
this.ts = opts.ts
|
|
24
|
-
|
|
25
|
-
/** A function specifying when a value should be considered as "no data" and thus not ignored. The line will have a break at these values.
|
|
26
|
-
* @type {function(string):boolean} */
|
|
27
|
-
this.noData = opts.noData || ((v) => v === undefined || v == '' || v === null || isNaN(+v))
|
|
28
|
-
|
|
29
|
-
//x
|
|
30
|
-
/** in geo unit
|
|
31
|
-
* @type {function(import("../core/Dataset.js").Cell,number,number):number} */
|
|
32
|
-
this.offsetX = opts.offsetX || ((c, r, z) => 0)
|
|
33
|
-
/** @type {function(import("../core/Dataset.js").Cell,number,number):number} */
|
|
34
|
-
this.width = opts.width || ((c, r, z) => r)
|
|
35
|
-
|
|
36
|
-
//y
|
|
37
|
-
/** in geo unit
|
|
38
|
-
* @type {function(import("../core/Dataset.js").Cell,number,number):number} */
|
|
39
|
-
this.offsetY = opts.offsetY || ((c, r, z) => 0)
|
|
40
|
-
/** @type {function(import("../core/Dataset.js").Cell,number,number):number} */
|
|
41
|
-
this.height = opts.height || ((c, r, z) => r)
|
|
42
|
-
/** @type {function(import("../core/Dataset.js").Cell,number,number):AnchorModeYEnum} */
|
|
43
|
-
this.anchorModeY = opts.anchorModeY || ((c, r, z) => 'center')
|
|
44
|
-
|
|
45
|
-
/** A function returning the width of the line, in geo unit
|
|
46
|
-
* @type {function(import('../core/Dataset.js').Cell, number, number, object):number} */
|
|
47
|
-
this.lineWidth = opts.lineWidth || ((v, r, s, z) => 1.5 * z)
|
|
48
|
-
|
|
49
|
-
/** A function returning the color of the chart.
|
|
50
|
-
* @type {function(import('../core/Dataset.js').Cell, number, number, object):string} */
|
|
51
|
-
this.color = opts.color || (() => 'black') //(c,r,z,vs) => {}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Draw cells as text.
|
|
56
|
-
*
|
|
57
|
-
* @param {Array.<import("../core/Dataset.js").Cell>} cells
|
|
58
|
-
* @param {import("../core/GeoCanvas.js").GeoCanvas} geoCanvas
|
|
59
|
-
* @param {number} resolution
|
|
60
|
-
*/
|
|
61
|
-
draw(cells, geoCanvas, resolution) {
|
|
62
|
-
//filter
|
|
63
|
-
if (this.filter) cells = cells.filter(this.filter)
|
|
64
|
-
|
|
65
|
-
//
|
|
66
|
-
const z = geoCanvas.view.z
|
|
67
|
-
const ctx = geoCanvas.offscreenCtx
|
|
68
|
-
|
|
69
|
-
//get view scale
|
|
70
|
-
const viewScale = this.viewScale ? this.viewScale(cells, resolution, z) : undefined
|
|
71
|
-
|
|
72
|
-
//compute cell amplitude
|
|
73
|
-
const getAmplitude = (c) => {
|
|
74
|
-
let min, max
|
|
75
|
-
for (let t of this.ts) {
|
|
76
|
-
const val = c[t]
|
|
77
|
-
if (val == undefined) continue
|
|
78
|
-
if (min == undefined || val < min) min = val
|
|
79
|
-
if (max == undefined || val > max) max = val
|
|
80
|
-
}
|
|
81
|
-
if (min == undefined) return undefined
|
|
82
|
-
return max - min
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
//compute max amplitude
|
|
86
|
-
let ampMax
|
|
87
|
-
for (let c of cells) {
|
|
88
|
-
const amp = getAmplitude(c)
|
|
89
|
-
if (amp == undefined) continue
|
|
90
|
-
if (ampMax == undefined || amp > ampMax) ampMax = amp
|
|
91
|
-
}
|
|
92
|
-
if (!ampMax) return
|
|
93
|
-
|
|
94
|
-
const nb = this.ts.length
|
|
95
|
-
|
|
96
|
-
ctx.lineCap = 'butt'
|
|
97
|
-
for (let c of cells) {
|
|
98
|
-
//line width
|
|
99
|
-
/** @type {number|undefined} */
|
|
100
|
-
const wG = this.lineWidth ? this.lineWidth(c, resolution, z, viewScale) : undefined
|
|
101
|
-
if (!wG || wG < 0) continue
|
|
102
|
-
|
|
103
|
-
//line color
|
|
104
|
-
/** @type {string|undefined} */
|
|
105
|
-
const col = this.color ? this.color(c, resolution, z, viewScale) : undefined
|
|
106
|
-
if (!col) continue
|
|
107
|
-
|
|
108
|
-
//x
|
|
109
|
-
const offX = this.offsetX ? this.offsetX(c, resolution, z) : 0
|
|
110
|
-
if (offX == undefined || isNaN(offX)) continue
|
|
111
|
-
const w = this.width ? this.width(c, resolution, z) : resolution
|
|
112
|
-
if (w == undefined || isNaN(w)) continue
|
|
113
|
-
|
|
114
|
-
//y
|
|
115
|
-
const offY = this.offsetY ? this.offsetY(c, resolution, z) : 0
|
|
116
|
-
if (offY == undefined || isNaN(offY)) continue
|
|
117
|
-
const h = this.height ? this.height(c, resolution, z) : resolution
|
|
118
|
-
if (h == undefined || isNaN(h)) continue
|
|
119
|
-
const anchY = this.anchorModeY ? this.anchorModeY(c, resolution, z) : 'center'
|
|
120
|
-
if (!anchY) continue
|
|
121
|
-
|
|
122
|
-
ctx.lineWidth = wG
|
|
123
|
-
ctx.strokeStyle = col
|
|
124
|
-
|
|
125
|
-
//compute anchor Y figures
|
|
126
|
-
let val0, y0
|
|
127
|
-
if (anchY === 'first') {
|
|
128
|
-
//get first value
|
|
129
|
-
val0 = c[this.ts[0]]
|
|
130
|
-
y0 = 0
|
|
131
|
-
} else if (anchY === 'last') {
|
|
132
|
-
//get last value
|
|
133
|
-
val0 = c[this.ts[this.ts.length - 1]]
|
|
134
|
-
y0 = 0
|
|
135
|
-
} else if (anchY === 'bottom') {
|
|
136
|
-
//get min
|
|
137
|
-
for (let t of this.ts) {
|
|
138
|
-
const val = +c[t]
|
|
139
|
-
if (val == undefined) continue
|
|
140
|
-
if (val0 == undefined || val < val0) val0 = val
|
|
141
|
-
}
|
|
142
|
-
y0 = 0
|
|
143
|
-
} else if (anchY === 'top') {
|
|
144
|
-
//get max
|
|
145
|
-
for (let t of this.ts) {
|
|
146
|
-
const val = +c[t]
|
|
147
|
-
if (val == undefined) continue
|
|
148
|
-
if (val0 == undefined || val > val0) val0 = val
|
|
149
|
-
}
|
|
150
|
-
y0 = resolution
|
|
151
|
-
} else if (anchY === 'center') {
|
|
152
|
-
//get min and max
|
|
153
|
-
let min, max
|
|
154
|
-
for (let t of this.ts) {
|
|
155
|
-
const val = c[t]
|
|
156
|
-
if (val == undefined) continue
|
|
157
|
-
if (min == undefined || val < min) min = val
|
|
158
|
-
if (max == undefined || val > max) max = val
|
|
159
|
-
}
|
|
160
|
-
val0 = (+max + +min) * 0.5
|
|
161
|
-
y0 = resolution / 2
|
|
162
|
-
} else {
|
|
163
|
-
console.log('Unexpected anchorModeY: ' + anchY)
|
|
164
|
-
continue
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/*/draw line
|
|
168
|
-
if (val0 == undefined || isNaN(val0)) continue
|
|
169
|
-
cg.ctx.beginPath()
|
|
170
|
-
const sX = w / (nb - 1)
|
|
171
|
-
for (let i = 0; i < nb; i++) {
|
|
172
|
-
const val = c[this.ts[i]]
|
|
173
|
-
if (val == undefined || isNaN(val)) break
|
|
174
|
-
if (i == 0)
|
|
175
|
-
cg.ctx.moveTo(c.x + i * sX + offX, c.y + y0 + (val - val0) * h / ampMax + offY)
|
|
176
|
-
else
|
|
177
|
-
cg.ctx.lineTo(c.x + i * sX + offX, c.y + y0 + (val - val0) * h / ampMax + offY)
|
|
178
|
-
}
|
|
179
|
-
cg.ctx.stroke()*/
|
|
180
|
-
|
|
181
|
-
//draw line, segment by segment
|
|
182
|
-
const sX = w / (nb - 1)
|
|
183
|
-
|
|
184
|
-
//handle first point
|
|
185
|
-
let v0 = c[this.ts[0]]
|
|
186
|
-
if (!this.noData(v0)) {
|
|
187
|
-
ctx.beginPath()
|
|
188
|
-
ctx.moveTo(c.x + offX, c.y + y0 + ((v0 - val0) * h) / ampMax + offY)
|
|
189
|
-
}
|
|
190
|
-
//console.log(v0, isNaN(v0))
|
|
191
|
-
|
|
192
|
-
let v1
|
|
193
|
-
for (let i = 1; i < nb; i++) {
|
|
194
|
-
v1 = c[this.ts[i]]
|
|
195
|
-
|
|
196
|
-
//draw segment from v0 to v1
|
|
197
|
-
|
|
198
|
-
//both points 'no data'
|
|
199
|
-
if (this.noData(v0) && this.noData(v1)) {
|
|
200
|
-
//second point 'no data'
|
|
201
|
-
} else if (!this.noData(v0) && this.noData(v1)) {
|
|
202
|
-
ctx.stroke()
|
|
203
|
-
|
|
204
|
-
//first point 'no data'
|
|
205
|
-
} else if (this.noData(v0) && !this.noData(v1)) {
|
|
206
|
-
ctx.beginPath()
|
|
207
|
-
ctx.moveTo(c.x + i * sX + offX, c.y + y0 + ((v1 - val0) * h) / ampMax + offY)
|
|
208
|
-
|
|
209
|
-
//both points have data: trace line
|
|
210
|
-
} else {
|
|
211
|
-
ctx.lineTo(c.x + i * sX + offX, c.y + y0 + ((v1 - val0) * h) / ampMax + offY)
|
|
212
|
-
//if it is the last point, stroke
|
|
213
|
-
if (i == nb - 1) ctx.stroke()
|
|
214
|
-
}
|
|
215
|
-
v0 = v1
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
//update legend, if any
|
|
220
|
-
this.updateLegends({ style: this, resolution: resolution, z: z, viewScale: viewScale })
|
|
221
|
-
}
|
|
222
|
-
}
|
|
1
|
+
//@ts-check
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
import { Style } from '../core/Style.js'
|
|
5
|
+
|
|
6
|
+
/** @typedef {"first"|"bottom"|"center"|"top"|"last"} AnchorModeYEnum */
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Show cell as timeseries chart
|
|
10
|
+
* Can be used for sparkline map of https://datagistips.hypotheses.org/488
|
|
11
|
+
*
|
|
12
|
+
* @module style
|
|
13
|
+
* @author Julien Gaffuri
|
|
14
|
+
*/
|
|
15
|
+
export class TimeSeriesStyle extends Style {
|
|
16
|
+
/** @param {object} opts */
|
|
17
|
+
constructor(opts) {
|
|
18
|
+
super(opts)
|
|
19
|
+
opts = opts || {}
|
|
20
|
+
|
|
21
|
+
/** The columns of the time series, ordered in chronological order.
|
|
22
|
+
* @type {Array.<string>} */
|
|
23
|
+
this.ts = opts.ts
|
|
24
|
+
|
|
25
|
+
/** A function specifying when a value should be considered as "no data" and thus not ignored. The line will have a break at these values.
|
|
26
|
+
* @type {function(string):boolean} */
|
|
27
|
+
this.noData = opts.noData || ((v) => v === undefined || v == '' || v === null || isNaN(+v))
|
|
28
|
+
|
|
29
|
+
//x
|
|
30
|
+
/** in geo unit
|
|
31
|
+
* @type {function(import("../core/Dataset.js").Cell,number,number):number} */
|
|
32
|
+
this.offsetX = opts.offsetX || ((c, r, z) => 0)
|
|
33
|
+
/** @type {function(import("../core/Dataset.js").Cell,number,number):number} */
|
|
34
|
+
this.width = opts.width || ((c, r, z) => r)
|
|
35
|
+
|
|
36
|
+
//y
|
|
37
|
+
/** in geo unit
|
|
38
|
+
* @type {function(import("../core/Dataset.js").Cell,number,number):number} */
|
|
39
|
+
this.offsetY = opts.offsetY || ((c, r, z) => 0)
|
|
40
|
+
/** @type {function(import("../core/Dataset.js").Cell,number,number):number} */
|
|
41
|
+
this.height = opts.height || ((c, r, z) => r)
|
|
42
|
+
/** @type {function(import("../core/Dataset.js").Cell,number,number):AnchorModeYEnum} */
|
|
43
|
+
this.anchorModeY = opts.anchorModeY || ((c, r, z) => 'center')
|
|
44
|
+
|
|
45
|
+
/** A function returning the width of the line, in geo unit
|
|
46
|
+
* @type {function(import('../core/Dataset.js').Cell, number, number, object):number} */
|
|
47
|
+
this.lineWidth = opts.lineWidth || ((v, r, s, z) => 1.5 * z)
|
|
48
|
+
|
|
49
|
+
/** A function returning the color of the chart.
|
|
50
|
+
* @type {function(import('../core/Dataset.js').Cell, number, number, object):string} */
|
|
51
|
+
this.color = opts.color || (() => 'black') //(c,r,z,vs) => {}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Draw cells as text.
|
|
56
|
+
*
|
|
57
|
+
* @param {Array.<import("../core/Dataset.js").Cell>} cells
|
|
58
|
+
* @param {import("../core/GeoCanvas.js").GeoCanvas} geoCanvas
|
|
59
|
+
* @param {number} resolution
|
|
60
|
+
*/
|
|
61
|
+
draw(cells, geoCanvas, resolution) {
|
|
62
|
+
//filter
|
|
63
|
+
if (this.filter) cells = cells.filter(this.filter)
|
|
64
|
+
|
|
65
|
+
//
|
|
66
|
+
const z = geoCanvas.view.z
|
|
67
|
+
const ctx = geoCanvas.offscreenCtx
|
|
68
|
+
|
|
69
|
+
//get view scale
|
|
70
|
+
const viewScale = this.viewScale ? this.viewScale(cells, resolution, z) : undefined
|
|
71
|
+
|
|
72
|
+
//compute cell amplitude
|
|
73
|
+
const getAmplitude = (c) => {
|
|
74
|
+
let min, max
|
|
75
|
+
for (let t of this.ts) {
|
|
76
|
+
const val = c[t]
|
|
77
|
+
if (val == undefined) continue
|
|
78
|
+
if (min == undefined || val < min) min = val
|
|
79
|
+
if (max == undefined || val > max) max = val
|
|
80
|
+
}
|
|
81
|
+
if (min == undefined) return undefined
|
|
82
|
+
return max - min
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
//compute max amplitude
|
|
86
|
+
let ampMax
|
|
87
|
+
for (let c of cells) {
|
|
88
|
+
const amp = getAmplitude(c)
|
|
89
|
+
if (amp == undefined) continue
|
|
90
|
+
if (ampMax == undefined || amp > ampMax) ampMax = amp
|
|
91
|
+
}
|
|
92
|
+
if (!ampMax) return
|
|
93
|
+
|
|
94
|
+
const nb = this.ts.length
|
|
95
|
+
|
|
96
|
+
ctx.lineCap = 'butt'
|
|
97
|
+
for (let c of cells) {
|
|
98
|
+
//line width
|
|
99
|
+
/** @type {number|undefined} */
|
|
100
|
+
const wG = this.lineWidth ? this.lineWidth(c, resolution, z, viewScale) : undefined
|
|
101
|
+
if (!wG || wG < 0) continue
|
|
102
|
+
|
|
103
|
+
//line color
|
|
104
|
+
/** @type {string|undefined} */
|
|
105
|
+
const col = this.color ? this.color(c, resolution, z, viewScale) : undefined
|
|
106
|
+
if (!col) continue
|
|
107
|
+
|
|
108
|
+
//x
|
|
109
|
+
const offX = this.offsetX ? this.offsetX(c, resolution, z) : 0
|
|
110
|
+
if (offX == undefined || isNaN(offX)) continue
|
|
111
|
+
const w = this.width ? this.width(c, resolution, z) : resolution
|
|
112
|
+
if (w == undefined || isNaN(w)) continue
|
|
113
|
+
|
|
114
|
+
//y
|
|
115
|
+
const offY = this.offsetY ? this.offsetY(c, resolution, z) : 0
|
|
116
|
+
if (offY == undefined || isNaN(offY)) continue
|
|
117
|
+
const h = this.height ? this.height(c, resolution, z) : resolution
|
|
118
|
+
if (h == undefined || isNaN(h)) continue
|
|
119
|
+
const anchY = this.anchorModeY ? this.anchorModeY(c, resolution, z) : 'center'
|
|
120
|
+
if (!anchY) continue
|
|
121
|
+
|
|
122
|
+
ctx.lineWidth = wG
|
|
123
|
+
ctx.strokeStyle = col
|
|
124
|
+
|
|
125
|
+
//compute anchor Y figures
|
|
126
|
+
let val0, y0
|
|
127
|
+
if (anchY === 'first') {
|
|
128
|
+
//get first value
|
|
129
|
+
val0 = c[this.ts[0]]
|
|
130
|
+
y0 = 0
|
|
131
|
+
} else if (anchY === 'last') {
|
|
132
|
+
//get last value
|
|
133
|
+
val0 = c[this.ts[this.ts.length - 1]]
|
|
134
|
+
y0 = 0
|
|
135
|
+
} else if (anchY === 'bottom') {
|
|
136
|
+
//get min
|
|
137
|
+
for (let t of this.ts) {
|
|
138
|
+
const val = +c[t]
|
|
139
|
+
if (val == undefined) continue
|
|
140
|
+
if (val0 == undefined || val < val0) val0 = val
|
|
141
|
+
}
|
|
142
|
+
y0 = 0
|
|
143
|
+
} else if (anchY === 'top') {
|
|
144
|
+
//get max
|
|
145
|
+
for (let t of this.ts) {
|
|
146
|
+
const val = +c[t]
|
|
147
|
+
if (val == undefined) continue
|
|
148
|
+
if (val0 == undefined || val > val0) val0 = val
|
|
149
|
+
}
|
|
150
|
+
y0 = resolution
|
|
151
|
+
} else if (anchY === 'center') {
|
|
152
|
+
//get min and max
|
|
153
|
+
let min, max
|
|
154
|
+
for (let t of this.ts) {
|
|
155
|
+
const val = c[t]
|
|
156
|
+
if (val == undefined) continue
|
|
157
|
+
if (min == undefined || val < min) min = val
|
|
158
|
+
if (max == undefined || val > max) max = val
|
|
159
|
+
}
|
|
160
|
+
val0 = (+max + +min) * 0.5
|
|
161
|
+
y0 = resolution / 2
|
|
162
|
+
} else {
|
|
163
|
+
console.log('Unexpected anchorModeY: ' + anchY)
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/*/draw line
|
|
168
|
+
if (val0 == undefined || isNaN(val0)) continue
|
|
169
|
+
cg.ctx.beginPath()
|
|
170
|
+
const sX = w / (nb - 1)
|
|
171
|
+
for (let i = 0; i < nb; i++) {
|
|
172
|
+
const val = c[this.ts[i]]
|
|
173
|
+
if (val == undefined || isNaN(val)) break
|
|
174
|
+
if (i == 0)
|
|
175
|
+
cg.ctx.moveTo(c.x + i * sX + offX, c.y + y0 + (val - val0) * h / ampMax + offY)
|
|
176
|
+
else
|
|
177
|
+
cg.ctx.lineTo(c.x + i * sX + offX, c.y + y0 + (val - val0) * h / ampMax + offY)
|
|
178
|
+
}
|
|
179
|
+
cg.ctx.stroke()*/
|
|
180
|
+
|
|
181
|
+
//draw line, segment by segment
|
|
182
|
+
const sX = w / (nb - 1)
|
|
183
|
+
|
|
184
|
+
//handle first point
|
|
185
|
+
let v0 = c[this.ts[0]]
|
|
186
|
+
if (!this.noData(v0)) {
|
|
187
|
+
ctx.beginPath()
|
|
188
|
+
ctx.moveTo(c.x + offX, c.y + y0 + ((v0 - val0) * h) / ampMax + offY)
|
|
189
|
+
}
|
|
190
|
+
//console.log(v0, isNaN(v0))
|
|
191
|
+
|
|
192
|
+
let v1
|
|
193
|
+
for (let i = 1; i < nb; i++) {
|
|
194
|
+
v1 = c[this.ts[i]]
|
|
195
|
+
|
|
196
|
+
//draw segment from v0 to v1
|
|
197
|
+
|
|
198
|
+
//both points 'no data'
|
|
199
|
+
if (this.noData(v0) && this.noData(v1)) {
|
|
200
|
+
//second point 'no data'
|
|
201
|
+
} else if (!this.noData(v0) && this.noData(v1)) {
|
|
202
|
+
ctx.stroke()
|
|
203
|
+
|
|
204
|
+
//first point 'no data'
|
|
205
|
+
} else if (this.noData(v0) && !this.noData(v1)) {
|
|
206
|
+
ctx.beginPath()
|
|
207
|
+
ctx.moveTo(c.x + i * sX + offX, c.y + y0 + ((v1 - val0) * h) / ampMax + offY)
|
|
208
|
+
|
|
209
|
+
//both points have data: trace line
|
|
210
|
+
} else {
|
|
211
|
+
ctx.lineTo(c.x + i * sX + offX, c.y + y0 + ((v1 - val0) * h) / ampMax + offY)
|
|
212
|
+
//if it is the last point, stroke
|
|
213
|
+
if (i == nb - 1) ctx.stroke()
|
|
214
|
+
}
|
|
215
|
+
v0 = v1
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
//update legend, if any
|
|
220
|
+
this.updateLegends({ style: this, resolution: resolution, z: z, viewScale: viewScale })
|
|
221
|
+
}
|
|
222
|
+
}
|