gridviz 3.0.37 → 3.0.39
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 +8491 -11551
- package/dist/gridviz.min.js +2 -2
- package/package.json +74 -74
- package/src/button/Button.js +2 -1
- package/src/button/FullscreenButton.js +2 -0
- package/src/button/ZoomButtons.js +3 -2
- package/src/core/GeoCanvas.js +34 -38
- package/src/core/Legend.js +145 -8
- package/src/core/Map.js +17 -2
- package/src/core/Style.js +48 -0
- package/src/core/Tooltip.js +3 -0
- package/src/layer/GridLayer.js +25 -5
- package/src/legend/ColorCategoryLegend.js +25 -9
- package/src/legend/ColorDiscreteLegend.js +19 -5
- package/src/legend/ColorLegend.js +11 -5
- package/src/legend/OrientationLegend.js +6 -3
- package/src/legend/SizeLegend.js +9 -2
- package/src/legend/TernaryLegend.js +14 -2
- package/src/style/ShapeColorSizeStyle.js +4 -0
- package/src/style/SquareColorCategoryWebGLStyle.js +9 -1
- package/src/utils/scale.js +30 -1
package/package.json
CHANGED
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "gridviz",
|
|
3
|
-
"version": "3.0.
|
|
4
|
-
"description": "Visualization tool for gridded statistics",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"statistics",
|
|
7
|
-
"grid",
|
|
8
|
-
"gridded",
|
|
9
|
-
"grids",
|
|
10
|
-
"gridded statistics",
|
|
11
|
-
"dataviz",
|
|
12
|
-
"csv",
|
|
13
|
-
"GIS",
|
|
14
|
-
"WebGL",
|
|
15
|
-
"d3",
|
|
16
|
-
"cartography",
|
|
17
|
-
"bertin",
|
|
18
|
-
"geospatial",
|
|
19
|
-
"mapping"
|
|
20
|
-
],
|
|
21
|
-
"homepage": "https://github.com/eurostat/gridviz",
|
|
22
|
-
"author": "Joseph Davies, Julien Gaffuri",
|
|
23
|
-
"license": "EUPL-1.2",
|
|
24
|
-
"repository": {
|
|
25
|
-
"type": "git",
|
|
26
|
-
"url": "git+https://github.com/eurostat/gridviz.git"
|
|
27
|
-
},
|
|
28
|
-
"bugs": {
|
|
29
|
-
"url": "https://github.com/eurostat/gridviz/issues",
|
|
30
|
-
"email": "Joseph.DAVIES@ext.ec.europa.eu"
|
|
31
|
-
},
|
|
32
|
-
"type": "module",
|
|
33
|
-
"main": "dist/gridviz.min.js",
|
|
34
|
-
"module": "dist/gridviz.min.js",
|
|
35
|
-
"exports": {
|
|
36
|
-
"umd": "./dist/gridviz.min.js",
|
|
37
|
-
"default": "./src/index.js"
|
|
38
|
-
},
|
|
39
|
-
"scripts": {
|
|
40
|
-
"start": "webpack --config webpack.config.dev.cjs",
|
|
41
|
-
"server": "webpack serve -c webpack.config.dev.cjs",
|
|
42
|
-
"doc": "jsdoc -c docs/jsdoc/jsdoc.conf.json",
|
|
43
|
-
"build-prod": "webpack --config webpack.config.prod.cjs",
|
|
44
|
-
"test": "jest",
|
|
45
|
-
"format": "prettier --write ."
|
|
46
|
-
},
|
|
47
|
-
"files": [
|
|
48
|
-
"dist/**/*.js",
|
|
49
|
-
"src/**/*.js"
|
|
50
|
-
],
|
|
51
|
-
"dependencies": {
|
|
52
|
-
"d3-array": "^3.2.4",
|
|
53
|
-
"d3-color": "^3.1.0",
|
|
54
|
-
"d3-fetch": "^3.0.1",
|
|
55
|
-
"d3-random": "^3.0.1",
|
|
56
|
-
"d3-scale": "^4.0.2",
|
|
57
|
-
"d3-selection": "^3.0.0",
|
|
58
|
-
"d3-zoom": "^3.0.0",
|
|
59
|
-
"jsdoc": "^4.0.4"
|
|
60
|
-
},
|
|
61
|
-
"devDependencies": {
|
|
62
|
-
"@babel/preset-env": "^7.25.4",
|
|
63
|
-
"babel-loader": "^8.1.0",
|
|
64
|
-
"jshint": "^2.12.0",
|
|
65
|
-
"prettier": "^2.8.8",
|
|
66
|
-
"webpack": "^5.96.1",
|
|
67
|
-
"webpack-cli": "^5.1.4",
|
|
68
|
-
"webpack-dev-server": "^5.1.0",
|
|
69
|
-
"webpack-livereload-plugin": "^3.0.2"
|
|
70
|
-
},
|
|
71
|
-
"engines": {
|
|
72
|
-
"node": ">=14"
|
|
73
|
-
}
|
|
74
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "gridviz",
|
|
3
|
+
"version": "3.0.39",
|
|
4
|
+
"description": "Visualization tool for gridded statistics",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"statistics",
|
|
7
|
+
"grid",
|
|
8
|
+
"gridded",
|
|
9
|
+
"grids",
|
|
10
|
+
"gridded statistics",
|
|
11
|
+
"dataviz",
|
|
12
|
+
"csv",
|
|
13
|
+
"GIS",
|
|
14
|
+
"WebGL",
|
|
15
|
+
"d3",
|
|
16
|
+
"cartography",
|
|
17
|
+
"bertin",
|
|
18
|
+
"geospatial",
|
|
19
|
+
"mapping"
|
|
20
|
+
],
|
|
21
|
+
"homepage": "https://github.com/eurostat/gridviz",
|
|
22
|
+
"author": "Joseph Davies, Julien Gaffuri",
|
|
23
|
+
"license": "EUPL-1.2",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/eurostat/gridviz.git"
|
|
27
|
+
},
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/eurostat/gridviz/issues",
|
|
30
|
+
"email": "Joseph.DAVIES@ext.ec.europa.eu"
|
|
31
|
+
},
|
|
32
|
+
"type": "module",
|
|
33
|
+
"main": "dist/gridviz.min.js",
|
|
34
|
+
"module": "dist/gridviz.min.js",
|
|
35
|
+
"exports": {
|
|
36
|
+
"umd": "./dist/gridviz.min.js",
|
|
37
|
+
"default": "./src/index.js"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"start": "webpack --config webpack.config.dev.cjs",
|
|
41
|
+
"server": "webpack serve -c webpack.config.dev.cjs",
|
|
42
|
+
"doc": "jsdoc -c docs/jsdoc/jsdoc.conf.json",
|
|
43
|
+
"build-prod": "webpack --config webpack.config.prod.cjs",
|
|
44
|
+
"test": "jest",
|
|
45
|
+
"format": "prettier --write ."
|
|
46
|
+
},
|
|
47
|
+
"files": [
|
|
48
|
+
"dist/**/*.js",
|
|
49
|
+
"src/**/*.js"
|
|
50
|
+
],
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"d3-array": "^3.2.4",
|
|
53
|
+
"d3-color": "^3.1.0",
|
|
54
|
+
"d3-fetch": "^3.0.1",
|
|
55
|
+
"d3-random": "^3.0.1",
|
|
56
|
+
"d3-scale": "^4.0.2",
|
|
57
|
+
"d3-selection": "^3.0.0",
|
|
58
|
+
"d3-zoom": "^3.0.0",
|
|
59
|
+
"jsdoc": "^4.0.4"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@babel/preset-env": "^7.25.4",
|
|
63
|
+
"babel-loader": "^8.1.0",
|
|
64
|
+
"jshint": "^2.12.0",
|
|
65
|
+
"prettier": "^2.8.8",
|
|
66
|
+
"webpack": "^5.96.1",
|
|
67
|
+
"webpack-cli": "^5.1.4",
|
|
68
|
+
"webpack-dev-server": "^5.1.0",
|
|
69
|
+
"webpack-livereload-plugin": "^3.0.2"
|
|
70
|
+
},
|
|
71
|
+
"engines": {
|
|
72
|
+
"node": ">=14"
|
|
73
|
+
}
|
|
74
|
+
}
|
package/src/button/Button.js
CHANGED
|
@@ -30,7 +30,8 @@ export class Button {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
if (opts.title) this.div.attr('title', opts.title)
|
|
33
|
-
|
|
33
|
+
this.div.classed('gridviz-button', true)
|
|
34
|
+
if (opts.class) for (const c of String(opts.class).split(/\s+/)) if (c) this.div.classed(c, true)
|
|
34
35
|
|
|
35
36
|
// add events
|
|
36
37
|
if (opts.onClickFunction) this.div.on('click', opts.onClickFunction)
|
|
@@ -24,10 +24,12 @@ export class FullscreenButton extends Button {
|
|
|
24
24
|
|
|
25
25
|
constructor(opts) {
|
|
26
26
|
super(opts)
|
|
27
|
+
this.div.classed('gridviz-fullscreen-button', true)
|
|
27
28
|
|
|
28
29
|
// append fullscreen icon to button container
|
|
29
30
|
this.div.node().innerHTML = `
|
|
30
31
|
<svg
|
|
32
|
+
class="gridviz-fullscreen-icon"
|
|
31
33
|
style="height: 1.2rem; width: 1.2rem; fill:black; margin:0;"
|
|
32
34
|
focusable="false"
|
|
33
35
|
aria-hidden="true"
|
|
@@ -15,11 +15,12 @@ export class ZoomButtons extends Button {
|
|
|
15
15
|
|
|
16
16
|
this.onZoom = opts.onZoom // custom user event handler
|
|
17
17
|
this.delta = opts.delta || 0.2
|
|
18
|
+
this.div.classed('gridviz-zoom-buttons', true)
|
|
18
19
|
|
|
19
20
|
// Create zoom in button
|
|
20
21
|
this.zoomInBtn = document.createElement('a')
|
|
21
22
|
this.zoomInBtn.id = 'zoom-in'
|
|
22
|
-
this.zoomInBtn.className = 'gridviz-zoom-button'
|
|
23
|
+
this.zoomInBtn.className = 'gridviz-zoom-button gridviz-zoom-button--in'
|
|
23
24
|
this.zoomInBtn.title = 'Zoom in'
|
|
24
25
|
this.zoomInBtn.textContent = '+'
|
|
25
26
|
this.zoomInBtn.addEventListener('click', (e) => {
|
|
@@ -35,7 +36,7 @@ export class ZoomButtons extends Button {
|
|
|
35
36
|
// Create zoom out button
|
|
36
37
|
this.zoomOutBtn = document.createElement('a')
|
|
37
38
|
this.zoomOutBtn.id = 'zoom-out'
|
|
38
|
-
this.zoomOutBtn.className = 'gridviz-zoom-button'
|
|
39
|
+
this.zoomOutBtn.className = 'gridviz-zoom-button gridviz-zoom-button--out'
|
|
39
40
|
this.zoomOutBtn.title = 'Zoom out'
|
|
40
41
|
this.zoomOutBtn.textContent = '-'
|
|
41
42
|
this.zoomOutBtn.addEventListener('click', (e) => {
|
package/src/core/GeoCanvas.js
CHANGED
|
@@ -114,6 +114,7 @@ export class GeoCanvas {
|
|
|
114
114
|
this.canvasSave.dx = 0
|
|
115
115
|
this.canvasSave.dy = 0
|
|
116
116
|
this.canvasSave.f = 1
|
|
117
|
+
this.canvasSave.view = { x: this.view.x, y: this.view.y, z: this.view.z }
|
|
117
118
|
if (this.onZoomStartFun) this.onZoomStartFun(e)
|
|
118
119
|
})
|
|
119
120
|
.on('end', (e) => {
|
|
@@ -179,7 +180,7 @@ export class GeoCanvas {
|
|
|
179
180
|
this.zoomExtent = opts.zoomExtent || [0, Infinity]
|
|
180
181
|
|
|
181
182
|
/** Canvas state, to be used to avoid unnecessary redraws on zoom/pan
|
|
182
|
-
* @type {{c:HTMLCanvasElement|null,dx:number,dy:number,f:number}} */
|
|
183
|
+
* @type {{c:HTMLCanvasElement|null,dx:number,dy:number,f:number,view?:{x:number,y:number,z:number}}} */
|
|
183
184
|
this.canvasSave = { c: null, dx: 0, dy: 0, f: 1 }
|
|
184
185
|
}
|
|
185
186
|
|
|
@@ -200,21 +201,7 @@ export class GeoCanvas {
|
|
|
200
201
|
this.updateExtentGeo()
|
|
201
202
|
|
|
202
203
|
if (this.canvasSave.c) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
// Update saved canvas offset
|
|
206
|
-
this.canvasSave.dx -= dxGeo * scale
|
|
207
|
-
this.canvasSave.dy += dyGeo * scale
|
|
208
|
-
|
|
209
|
-
// clear canvas
|
|
210
|
-
this.clear(this.backgroundColor)
|
|
211
|
-
|
|
212
|
-
// this doesnt work on mobile https://github.com/eurostat/gridviz/issues/98
|
|
213
|
-
//this.ctx.drawImage(this.canvasSave.c, this.canvasSave.dx, this.canvasSave.dy)
|
|
214
|
-
this.offscreenCtx.drawImage(this.canvasSave.c, this.canvasSave.dx, this.canvasSave.dy)
|
|
215
|
-
|
|
216
|
-
// Render the offscreen canvas to the visible context
|
|
217
|
-
this.ctx.drawImage(this.offscreenCtx.canvas, 0, 0)
|
|
204
|
+
this._drawSnapshot()
|
|
218
205
|
} else {
|
|
219
206
|
console.log('no canvas save')
|
|
220
207
|
}
|
|
@@ -258,29 +245,38 @@ export class GeoCanvas {
|
|
|
258
245
|
this._drawZoomFrame(f, xGeo, yGeo)
|
|
259
246
|
}
|
|
260
247
|
|
|
261
|
-
_drawZoomFrame(
|
|
248
|
+
_drawZoomFrame() {
|
|
262
249
|
// zoom in on the current canvas state
|
|
263
|
-
if (this.canvasSave.c)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
250
|
+
if (this.canvasSave.c) this._drawSnapshot()
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Draw the canvas saved when the pan/zoom started where it belongs in the current view.
|
|
255
|
+
* It is placed from the two views (the one it was taken at and the current one) rather than
|
|
256
|
+
* accumulated from each step: the zoom anchor follows the mouse, and an accumulation that
|
|
257
|
+
* assumes it stays put leaves the picture away from where the final redraw puts it.
|
|
258
|
+
* @private
|
|
259
|
+
*/
|
|
260
|
+
_drawSnapshot() {
|
|
261
|
+
const sv = this.canvasSave.view
|
|
262
|
+
const f = sv ? sv.z / this.view.z : 1
|
|
263
|
+
// where the saved canvas' top left corner is in the current view
|
|
264
|
+
const dx = sv ? (sv.x - (this.w * 0.5) * sv.z - this.view.x) / this.view.z + this.w * 0.5 : 0
|
|
265
|
+
const dy = sv ? this.h * 0.5 - (sv.y + (this.h * 0.5) * sv.z - this.view.y) / this.view.z : 0
|
|
266
|
+
this.canvasSave.f = f
|
|
267
|
+
this.canvasSave.dx = dx
|
|
268
|
+
this.canvasSave.dy = dy
|
|
269
|
+
|
|
270
|
+
this.clear(this.backgroundColor)
|
|
271
|
+
// this doesnt work on mobile https://github.com/eurostat/gridviz/issues/98 : drawn on the offscreen canvas first
|
|
272
|
+
this.offscreenCtx.drawImage(
|
|
273
|
+
this.canvasSave.c,
|
|
274
|
+
dx,
|
|
275
|
+
dy,
|
|
276
|
+
f * this.canvasSave.c.width,
|
|
277
|
+
f * this.canvasSave.c.height
|
|
278
|
+
)
|
|
279
|
+
this.ctx.drawImage(this.offscreenCanvas, 0, 0, this.canvas.width, this.canvas.height)
|
|
284
280
|
}
|
|
285
281
|
|
|
286
282
|
/**
|
package/src/core/Legend.js
CHANGED
|
@@ -54,13 +54,30 @@ export class Legend {
|
|
|
54
54
|
if (this.id) this.div.attr('id', this.id)
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// Which style options the caller set. The ones they didn't set are written as a CSS variable with the
|
|
58
|
+
// usual value as its fallback (--gridviz-legend-font-family, ...): nothing changes unless a page defines the variable.
|
|
59
|
+
/** @private */
|
|
60
|
+
this._set = {
|
|
61
|
+
padding: opts.padding !== undefined,
|
|
62
|
+
fontFamily: opts['font-family'] !== undefined,
|
|
63
|
+
titleFontSize: opts.titleFontSize !== undefined,
|
|
64
|
+
titleFontWeight: opts.titleFontWeight !== undefined,
|
|
65
|
+
labelFontSize: opts.labelFontSize !== undefined,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// CSS classes, to style legends from a stylesheet: gridviz-legend on every legend, a modifier for the
|
|
69
|
+
// type (see each legend class), and whatever the caller asks for with the className option.
|
|
70
|
+
this.div.classed('gridviz-legend', true)
|
|
71
|
+
if (opts.className)
|
|
72
|
+
for (const c of String(opts.className).split(/\s+/)) if (c) this.div.classed(c, true)
|
|
73
|
+
|
|
57
74
|
//set style
|
|
58
75
|
this.div.style('background', this.background)
|
|
59
|
-
this.div.style('padding', this.padding)
|
|
76
|
+
this.div.style('padding', this._css('padding', this.padding, this._set.padding))
|
|
60
77
|
this.div.style('border', this.border)
|
|
61
78
|
this.div.style('border-radius', this['border-radius'])
|
|
62
79
|
this.div.style('box-shadow', this['box-shadow'])
|
|
63
|
-
this.div.style('font-family', this['font-family'])
|
|
80
|
+
this.div.style('font-family', this._css('font-family', this['font-family'], this._set.fontFamily))
|
|
64
81
|
|
|
65
82
|
//if (this.width) this.div.style('width', this.width)
|
|
66
83
|
//if (this.height) this.div.style('height', this.height)
|
|
@@ -74,16 +91,136 @@ export class Legend {
|
|
|
74
91
|
this.labelFontSize = opts.labelFontSize || '0.8em'
|
|
75
92
|
this.labelUnitText = opts.labelUnitText || ''
|
|
76
93
|
this.labelFormat = opts.labelFormat
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Whether the title and the labels are HTML (to italicise a word, for example) instead of plain text.
|
|
97
|
+
* Off by default, which is plain text. Only turn it on for text you trust: it is not escaped.
|
|
98
|
+
* @type {boolean} */
|
|
99
|
+
this.labelsAsHTML = opts.labelsAsHTML === true
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Whether hovering a swatch of the legend shows only the matching cells on the map.
|
|
103
|
+
* @type {boolean} */
|
|
104
|
+
this.interactive = opts.interactive !== false
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The style the legend belongs to, and a function to redraw the map. Both set by the layer when it draws.
|
|
108
|
+
* @type {import('./Style.js').Style|undefined} */
|
|
109
|
+
this.style_ = undefined
|
|
110
|
+
/** @type {function|undefined} */
|
|
111
|
+
this.redraw = undefined
|
|
112
|
+
/**
|
|
113
|
+
* The element the legends are drawn in, set by the layer when it draws.
|
|
114
|
+
* @type {HTMLElement|undefined} */
|
|
115
|
+
this.container = undefined
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Draw at the width of the container instead of this.width (width:'fill').
|
|
119
|
+
* @type {boolean} @protected */
|
|
120
|
+
this.fill = opts.width === 'fill'
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Show only the cells drawn in some colours on the map, or all of them again.
|
|
125
|
+
* @param {Array.<string>|null} colors
|
|
126
|
+
*/
|
|
127
|
+
highlight(colors) {
|
|
128
|
+
if (!this.interactive || !this.style_) return
|
|
129
|
+
const before = this.style_.highlightColors
|
|
130
|
+
// flag the legend while hovered, so that the map's redraw leaves its element in place:
|
|
131
|
+
// a hovered element that is detached and appended again never gets its mouseleave
|
|
132
|
+
if (this.div) this.div.attr('data-gridviz-hovered', colors && colors.length ? '' : null)
|
|
133
|
+
this.style_.setHighlight(colors)
|
|
134
|
+
// nothing to redraw when the highlight didn't change (mouse moving between a swatch's parts)
|
|
135
|
+
if (before === null && this.style_.highlightColors === null) return
|
|
136
|
+
if (this.redraw) this.redraw()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Make a swatch react to the mouse: hovering it highlights the cells of a colour on the map.
|
|
141
|
+
* @param {*} selection The d3 selection of the swatch (any element)
|
|
142
|
+
* @param {string} color The colour the swatch stands for
|
|
143
|
+
* @param {{enter?:function, leave?:function}} [extra] Extra feedback on the swatch itself
|
|
144
|
+
*/
|
|
145
|
+
bindHover(selection, color, extra) {
|
|
146
|
+
if (!this.interactive) return
|
|
147
|
+
selection
|
|
148
|
+
.style('cursor', 'pointer')
|
|
149
|
+
.on('mouseenter', () => {
|
|
150
|
+
if (extra && extra.enter) extra.enter()
|
|
151
|
+
this.highlight([color])
|
|
152
|
+
})
|
|
153
|
+
.on('mouseleave', () => {
|
|
154
|
+
if (extra && extra.leave) extra.leave()
|
|
155
|
+
this.highlight(null)
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A style value with a CSS variable around it, unless the caller set the option themselves.
|
|
161
|
+
* @param {string} name The variable's name, after --gridviz-legend-
|
|
162
|
+
* @param {string} value The value to use when the variable isn't defined
|
|
163
|
+
* @param {boolean} custom
|
|
164
|
+
* @returns {string}
|
|
165
|
+
* @protected
|
|
166
|
+
*/
|
|
167
|
+
_css(name, value, custom) {
|
|
168
|
+
return custom ? value : `var(--gridviz-legend-${name}, ${value})`
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The font size of the labels, for the style() of a label element.
|
|
172
|
+
* @returns {string}
|
|
173
|
+
* @protected */
|
|
174
|
+
labelFontSizeCSS() {
|
|
175
|
+
return this._css('label-font-size', this.labelFontSize, this._set.labelFontSize)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Mark the root element as a type of legend: the CSS class gridviz-legend--<type>.
|
|
180
|
+
* @param {string} type
|
|
181
|
+
* @protected
|
|
182
|
+
*/
|
|
183
|
+
setType(type) {
|
|
184
|
+
this.div.classed('gridviz-legend--' + type, true)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The width to draw at: this.width, or with width:'fill' the width of the container the legends are in
|
|
189
|
+
* (the legendContainer of the map), less this legend's own padding.
|
|
190
|
+
* @param {number} fallback Used when the container has no width (yet, or hidden)
|
|
191
|
+
* @returns {number}
|
|
192
|
+
* @protected
|
|
193
|
+
*/
|
|
194
|
+
availableWidth(fallback) {
|
|
195
|
+
if (!this.fill) return this.width
|
|
196
|
+
const container = this.container
|
|
197
|
+
const w = container && container.clientWidth
|
|
198
|
+
if (!w) return fallback
|
|
199
|
+
const p = parseFloat(this.padding) || 0
|
|
200
|
+
return Math.max(60, Math.floor(w - 2 * p))
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Set the text of a d3 selection: as HTML if the legend has labelsAsHTML, as plain text otherwise.
|
|
205
|
+
* @param {*} selection
|
|
206
|
+
* @param {string} text
|
|
207
|
+
* @protected
|
|
208
|
+
*/
|
|
209
|
+
setText(selection, text) {
|
|
210
|
+
return this.labelsAsHTML ? selection.html(text) : selection.text(text)
|
|
77
211
|
}
|
|
78
212
|
|
|
79
213
|
makeTitle() {
|
|
80
214
|
if (!this.title) return
|
|
81
|
-
this.
|
|
82
|
-
.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
215
|
+
this.setText(
|
|
216
|
+
this.div
|
|
217
|
+
.append('div')
|
|
218
|
+
.classed('gridviz-legend-title', true)
|
|
219
|
+
.style('font-size', this._css('title-font-size', this.titleFontSize, this._set.titleFontSize))
|
|
220
|
+
.style('font-weight', this._css('title-font-weight', this.titleFontWeight, this._set.titleFontWeight))
|
|
221
|
+
.style('margin-bottom', '7px'),
|
|
222
|
+
this.title
|
|
223
|
+
)
|
|
87
224
|
}
|
|
88
225
|
|
|
89
226
|
/**
|
package/src/core/Map.js
CHANGED
|
@@ -46,6 +46,7 @@ export class Map {
|
|
|
46
46
|
console.error('Cannot find gridviz container element.')
|
|
47
47
|
return
|
|
48
48
|
}
|
|
49
|
+
this.container.classList.add('gridviz-map')
|
|
49
50
|
|
|
50
51
|
//https://css-tricks.com/absolute-positioning-inside-relative-positioning/
|
|
51
52
|
this.container.style.position = 'relative' // container element must have relative positioning
|
|
@@ -87,6 +88,13 @@ export class Map {
|
|
|
87
88
|
? select(opts.legendContainer) // Wrap the provided HTML node in a D3 selection
|
|
88
89
|
: null
|
|
89
90
|
this._legendProvidedByUser = !!opts.legendContainer
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Called with the legend container (an HTML element) each time the map has drawn and its legends
|
|
94
|
+
* are in the container, for a host that needs to adjust them (fit their width, restyle...).
|
|
95
|
+
* It can also be set after the map is created.
|
|
96
|
+
* @type {function(HTMLElement):void|undefined} */
|
|
97
|
+
this.onLegendUpdate = opts.onLegendUpdate
|
|
90
98
|
if (!this.legend) this.initialiseLegend()
|
|
91
99
|
|
|
92
100
|
//tooltip
|
|
@@ -167,6 +175,7 @@ export class Map {
|
|
|
167
175
|
*/
|
|
168
176
|
initialiseCanvas() {
|
|
169
177
|
const canvas = document.createElement('canvas')
|
|
178
|
+
canvas.classList.add('gridviz-canvas')
|
|
170
179
|
canvas.setAttribute('width', '' + this.w)
|
|
171
180
|
canvas.setAttribute('height', '' + this.h)
|
|
172
181
|
this.container.appendChild(canvas)
|
|
@@ -177,6 +186,7 @@ export class Map {
|
|
|
177
186
|
this.legend = select(this.container)
|
|
178
187
|
.append('div') // Create a new container
|
|
179
188
|
.attr('id', 'gridviz-legend')
|
|
189
|
+
.classed('gridviz-legends', true)
|
|
180
190
|
.style('position', 'absolute')
|
|
181
191
|
.style('width', 'auto')
|
|
182
192
|
.style('height', 'auto')
|
|
@@ -232,8 +242,10 @@ export class Map {
|
|
|
232
242
|
// a destroyed map can still be asked to redraw by a dataset it shared (see destroy)
|
|
233
243
|
if (this._destroyed) return this
|
|
234
244
|
|
|
235
|
-
//remove legend elements
|
|
236
|
-
|
|
245
|
+
//remove legend elements: the legends themselves only, not what is inside them
|
|
246
|
+
//(a legend keeps its content while one of its swatches is hovered, see Style.updateLegends)
|
|
247
|
+
//except the one being hovered, which would lose its mouseleave (see Legend.highlight)
|
|
248
|
+
if (this.legend) this.legend.selectAll(':scope > *:not([data-gridviz-hovered])').remove()
|
|
237
249
|
|
|
238
250
|
//clear
|
|
239
251
|
this.geoCanvas.initCanvasTransform()
|
|
@@ -277,6 +289,9 @@ export class Map {
|
|
|
277
289
|
// the canvas holds no focus rectangle any more
|
|
278
290
|
this._focusedCell = undefined
|
|
279
291
|
|
|
292
|
+
// the legends are in the container now
|
|
293
|
+
if (this.onLegendUpdate && this.legend) this.onLegendUpdate(this.legend.node())
|
|
294
|
+
|
|
280
295
|
return this
|
|
281
296
|
}
|
|
282
297
|
|
package/src/core/Style.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
//@ts-check
|
|
2
2
|
'use strict'
|
|
3
3
|
|
|
4
|
+
import { color as d3color } from 'd3-color'
|
|
4
5
|
import { Drawable } from './Drawable.js'
|
|
5
6
|
|
|
6
7
|
/** @typedef {"square"|"circle"|"diamond"|"donut"|"triangle_up"|"triangle_down"|"triangle_left"|"triangle_right"|"none"} Shape */
|
|
@@ -47,6 +48,48 @@ export class Style extends Drawable {
|
|
|
47
48
|
* @public
|
|
48
49
|
* @type {Array.<import("./Legend").Legend>} */
|
|
49
50
|
this.legends = []
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The colours of the cells to show while a legend swatch is hovered (see Legend.highlight),
|
|
54
|
+
* normalised with Style.normalizeColor. Null when nothing is highlighted.
|
|
55
|
+
* Styles that draw cells in a colour per class honour it with isHighlighted.
|
|
56
|
+
* @type {Array.<string>|null} */
|
|
57
|
+
this.highlightColors = null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A colour as a canonical string, so that '#ccc', 'rgb(204,204,204)' and '#cccccc' compare equal.
|
|
62
|
+
* @param {string} c
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
static normalizeColor(c) {
|
|
66
|
+
let n = Style._normalizedColors.get(c)
|
|
67
|
+
if (n === undefined) {
|
|
68
|
+
const parsed = d3color(c)
|
|
69
|
+
n = parsed ? parsed.formatRgb() : String(c)
|
|
70
|
+
// a style drawing millions of distinct colours must not grow this without bound
|
|
71
|
+
if (Style._normalizedColors.size > 10000) Style._normalizedColors.clear()
|
|
72
|
+
Style._normalizedColors.set(c, n)
|
|
73
|
+
}
|
|
74
|
+
return n
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Show only the cells drawn in some colours: what hovering a legend swatch does.
|
|
79
|
+
* The map has to be redrawn for it to show (Legend.highlight does).
|
|
80
|
+
* @param {Array.<string>|null|undefined} colors The colours to show, or null to show everything again
|
|
81
|
+
* @returns {this} */
|
|
82
|
+
setHighlight(colors) {
|
|
83
|
+
this.highlightColors = colors && colors.length ? colors.map(Style.normalizeColor) : null
|
|
84
|
+
return this
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Whether cells drawn in a colour are shown: always, unless a legend swatch is hovered and it is another colour.
|
|
89
|
+
* @param {string} c
|
|
90
|
+
* @returns {boolean} */
|
|
91
|
+
isHighlighted(c) {
|
|
92
|
+
return !this.highlightColors || this.highlightColors.includes(Style.normalizeColor(c))
|
|
50
93
|
}
|
|
51
94
|
|
|
52
95
|
/**
|
|
@@ -78,6 +121,8 @@ export class Style extends Drawable {
|
|
|
78
121
|
* @param {object} opts
|
|
79
122
|
* @returns {this} */
|
|
80
123
|
updateLegends(opts) {
|
|
124
|
+
// the legend being hovered must keep its elements, or it would lose the mouse
|
|
125
|
+
if (this.highlightColors) return this
|
|
81
126
|
Style.updateLegendsRecursive(this.legends, opts)
|
|
82
127
|
return this
|
|
83
128
|
}
|
|
@@ -96,3 +141,6 @@ export class Style extends Drawable {
|
|
|
96
141
|
return this
|
|
97
142
|
}
|
|
98
143
|
}
|
|
144
|
+
|
|
145
|
+
/** @type {Map<string,string>} */
|
|
146
|
+
Style._normalizedColors = new Map()
|
package/src/core/Tooltip.js
CHANGED
|
@@ -75,6 +75,9 @@ export class Tooltip {
|
|
|
75
75
|
this.tooltip.style('left', '0')
|
|
76
76
|
this.tooltip.style('top', '0')
|
|
77
77
|
|
|
78
|
+
// a tooltip element given by the user gets the class too
|
|
79
|
+
this.tooltip.classed('gridviz-tooltip', true)
|
|
80
|
+
|
|
78
81
|
// aria-labels (thanks to wahlatlas)
|
|
79
82
|
this.tooltip.attr('role', 'tooltip').attr('aria-live', 'polite')
|
|
80
83
|
}
|