gridviz 3.0.37 → 3.0.38
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 +8400 -11513
- package/dist/gridviz.min.js +2 -2
- package/package.json +74 -74
- package/src/core/Legend.js +142 -8
- package/src/core/Map.js +13 -2
- package/src/core/Style.js +48 -0
- package/src/layer/GridLayer.js +24 -5
- package/src/legend/ColorCategoryLegend.js +21 -9
- package/src/legend/ColorDiscreteLegend.js +18 -5
- package/src/legend/ColorLegend.js +9 -3
- package/src/legend/OrientationLegend.js +4 -2
- package/src/legend/SizeLegend.js +4 -2
- package/src/legend/TernaryLegend.js +10 -0
- package/src/style/ShapeColorSizeStyle.js +4 -0
- package/src/style/SquareColorCategoryWebGLStyle.js +9 -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.38",
|
|
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/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,133 @@ 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
|
+
this.style_.setHighlight(colors)
|
|
131
|
+
// nothing to redraw when the highlight didn't change (mouse moving between a swatch's parts)
|
|
132
|
+
if (before === null && this.style_.highlightColors === null) return
|
|
133
|
+
if (this.redraw) this.redraw()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Make a swatch react to the mouse: hovering it highlights the cells of a colour on the map.
|
|
138
|
+
* @param {*} selection The d3 selection of the swatch (any element)
|
|
139
|
+
* @param {string} color The colour the swatch stands for
|
|
140
|
+
* @param {{enter?:function, leave?:function}} [extra] Extra feedback on the swatch itself
|
|
141
|
+
*/
|
|
142
|
+
bindHover(selection, color, extra) {
|
|
143
|
+
if (!this.interactive) return
|
|
144
|
+
selection
|
|
145
|
+
.style('cursor', 'pointer')
|
|
146
|
+
.on('mouseenter', () => {
|
|
147
|
+
if (extra && extra.enter) extra.enter()
|
|
148
|
+
this.highlight([color])
|
|
149
|
+
})
|
|
150
|
+
.on('mouseleave', () => {
|
|
151
|
+
if (extra && extra.leave) extra.leave()
|
|
152
|
+
this.highlight(null)
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* A style value with a CSS variable around it, unless the caller set the option themselves.
|
|
158
|
+
* @param {string} name The variable's name, after --gridviz-legend-
|
|
159
|
+
* @param {string} value The value to use when the variable isn't defined
|
|
160
|
+
* @param {boolean} custom
|
|
161
|
+
* @returns {string}
|
|
162
|
+
* @protected
|
|
163
|
+
*/
|
|
164
|
+
_css(name, value, custom) {
|
|
165
|
+
return custom ? value : `var(--gridviz-legend-${name}, ${value})`
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The font size of the labels, for the style() of a label element.
|
|
169
|
+
* @returns {string}
|
|
170
|
+
* @protected */
|
|
171
|
+
labelFontSizeCSS() {
|
|
172
|
+
return this._css('label-font-size', this.labelFontSize, this._set.labelFontSize)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Mark the root element as a type of legend: the CSS class gridviz-legend--<type>.
|
|
177
|
+
* @param {string} type
|
|
178
|
+
* @protected
|
|
179
|
+
*/
|
|
180
|
+
setType(type) {
|
|
181
|
+
this.div.classed('gridviz-legend--' + type, true)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The width to draw at: this.width, or with width:'fill' the width of the container the legends are in
|
|
186
|
+
* (the legendContainer of the map), less this legend's own padding.
|
|
187
|
+
* @param {number} fallback Used when the container has no width (yet, or hidden)
|
|
188
|
+
* @returns {number}
|
|
189
|
+
* @protected
|
|
190
|
+
*/
|
|
191
|
+
availableWidth(fallback) {
|
|
192
|
+
if (!this.fill) return this.width
|
|
193
|
+
const container = this.container
|
|
194
|
+
const w = container && container.clientWidth
|
|
195
|
+
if (!w) return fallback
|
|
196
|
+
const p = parseFloat(this.padding) || 0
|
|
197
|
+
return Math.max(60, Math.floor(w - 2 * p))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Set the text of a d3 selection: as HTML if the legend has labelsAsHTML, as plain text otherwise.
|
|
202
|
+
* @param {*} selection
|
|
203
|
+
* @param {string} text
|
|
204
|
+
* @protected
|
|
205
|
+
*/
|
|
206
|
+
setText(selection, text) {
|
|
207
|
+
return this.labelsAsHTML ? selection.html(text) : selection.text(text)
|
|
77
208
|
}
|
|
78
209
|
|
|
79
210
|
makeTitle() {
|
|
80
211
|
if (!this.title) return
|
|
81
|
-
this.
|
|
82
|
-
.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
212
|
+
this.setText(
|
|
213
|
+
this.div
|
|
214
|
+
.append('div')
|
|
215
|
+
.classed('gridviz-legend-title', true)
|
|
216
|
+
.style('font-size', this._css('title-font-size', this.titleFontSize, this._set.titleFontSize))
|
|
217
|
+
.style('font-weight', this._css('title-font-weight', this.titleFontWeight, this._set.titleFontWeight))
|
|
218
|
+
.style('margin-bottom', '7px'),
|
|
219
|
+
this.title
|
|
220
|
+
)
|
|
87
221
|
}
|
|
88
222
|
|
|
89
223
|
/**
|
package/src/core/Map.js
CHANGED
|
@@ -87,6 +87,13 @@ export class Map {
|
|
|
87
87
|
? select(opts.legendContainer) // Wrap the provided HTML node in a D3 selection
|
|
88
88
|
: null
|
|
89
89
|
this._legendProvidedByUser = !!opts.legendContainer
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Called with the legend container (an HTML element) each time the map has drawn and its legends
|
|
93
|
+
* are in the container, for a host that needs to adjust them (fit their width, restyle...).
|
|
94
|
+
* It can also be set after the map is created.
|
|
95
|
+
* @type {function(HTMLElement):void|undefined} */
|
|
96
|
+
this.onLegendUpdate = opts.onLegendUpdate
|
|
90
97
|
if (!this.legend) this.initialiseLegend()
|
|
91
98
|
|
|
92
99
|
//tooltip
|
|
@@ -232,8 +239,9 @@ export class Map {
|
|
|
232
239
|
// a destroyed map can still be asked to redraw by a dataset it shared (see destroy)
|
|
233
240
|
if (this._destroyed) return this
|
|
234
241
|
|
|
235
|
-
//remove legend elements
|
|
236
|
-
|
|
242
|
+
//remove legend elements: the legends themselves only, not what is inside them
|
|
243
|
+
//(a legend keeps its content while one of its swatches is hovered, see Style.updateLegends)
|
|
244
|
+
if (this.legend) this.legend.selectAll(':scope > *').remove()
|
|
237
245
|
|
|
238
246
|
//clear
|
|
239
247
|
this.geoCanvas.initCanvasTransform()
|
|
@@ -277,6 +285,9 @@ export class Map {
|
|
|
277
285
|
// the canvas holds no focus rectangle any more
|
|
278
286
|
this._focusedCell = undefined
|
|
279
287
|
|
|
288
|
+
// the legends are in the container now
|
|
289
|
+
if (this.onLegendUpdate && this.legend) this.onLegendUpdate(this.legend.node())
|
|
290
|
+
|
|
280
291
|
return this
|
|
281
292
|
}
|
|
282
293
|
|
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/layer/GridLayer.js
CHANGED
|
@@ -68,6 +68,9 @@ export class GridLayer extends Layer {
|
|
|
68
68
|
if (this.blendOperation) ctx.globalCompositeOperation = this.blendOperation(z)
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// tell the legends where they are drawn, so that they can fit it (width:'fill')
|
|
72
|
+
if (legend) for (const s of this.styles) GridLayer.setLegendContainer(legend.node(), s.legends, s['styles'])
|
|
73
|
+
|
|
71
74
|
//draw cells, style by style
|
|
72
75
|
for (const s of this.styles) {
|
|
73
76
|
//check if style is visible
|
|
@@ -102,14 +105,14 @@ export class GridLayer extends Layer {
|
|
|
102
105
|
for (const s of this.styles) {
|
|
103
106
|
//check if style is visible
|
|
104
107
|
if (s.visible && !s.visible(z)) continue
|
|
105
|
-
GridLayer.addLegends(legend, s.legends)
|
|
108
|
+
GridLayer.addLegends(legend, s.legends, s, geoCanvas)
|
|
106
109
|
|
|
107
110
|
//case for styles of styles, like kernel smoothing
|
|
108
111
|
//TODO do better
|
|
109
112
|
if (s['styles']) {
|
|
110
113
|
for (const s2 of s['styles']) {
|
|
111
114
|
if (s2.visible && !s2.visible(z)) continue
|
|
112
|
-
GridLayer.addLegends(legend, s2.legends)
|
|
115
|
+
GridLayer.addLegends(legend, s2.legends, s2, geoCanvas)
|
|
113
116
|
}
|
|
114
117
|
}
|
|
115
118
|
}
|
|
@@ -117,9 +120,25 @@ export class GridLayer extends Layer {
|
|
|
117
120
|
}
|
|
118
121
|
|
|
119
122
|
/** @private */
|
|
120
|
-
static
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
static setLegendContainer(container, legends, subStyles) {
|
|
124
|
+
// legends can be nested in arrays, like in Style.updateLegendsRecursive
|
|
125
|
+
const set = (lg) => {
|
|
126
|
+
if (Array.isArray(lg)) lg.forEach(set)
|
|
127
|
+
else if (lg) lg.container = container
|
|
128
|
+
}
|
|
129
|
+
set(legends)
|
|
130
|
+
for (const s2 of subStyles || []) GridLayer.setLegendContainer(container, s2.legends, undefined)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** @private */
|
|
134
|
+
static addLegends(legendComp, lg, style, geoCanvas) {
|
|
135
|
+
if (Array.isArray(lg)) for (const lg_ of lg) this.addLegends(legendComp, lg_, style, geoCanvas)
|
|
136
|
+
else {
|
|
137
|
+
// so that hovering a swatch can highlight the cells of the style on the map
|
|
138
|
+
lg.style_ = style
|
|
139
|
+
lg.redraw = () => (geoCanvas.redraw ? geoCanvas.redraw() : undefined)
|
|
140
|
+
legendComp.node().append(lg.div.node())
|
|
141
|
+
}
|
|
123
142
|
}
|
|
124
143
|
|
|
125
144
|
/**
|
|
@@ -28,6 +28,7 @@ export class ColorCategoryLegend extends Legend {
|
|
|
28
28
|
this.dimension = opts.dimension || { r: 8 }
|
|
29
29
|
this.strokeColor = opts.strokeColor || 'gray'
|
|
30
30
|
this.strokeWidth = opts.strokeWidth || 1
|
|
31
|
+
this.setType('category')
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/**
|
|
@@ -47,7 +48,12 @@ export class ColorCategoryLegend extends Legend {
|
|
|
47
48
|
const cat = this.colorLabel[i]
|
|
48
49
|
|
|
49
50
|
//make div for category
|
|
50
|
-
const d = this.div.append('div')
|
|
51
|
+
const d = this.div.append('div').classed('gridviz-legend-category', true)
|
|
52
|
+
// hovering a category shows only its cells on the map
|
|
53
|
+
this.bindHover(d, cat[0], {
|
|
54
|
+
enter: () => d.classed('gridviz-legend-category--active', true).select('svg').select('rect, circle').style('stroke', 'red'),
|
|
55
|
+
leave: () => d.classed('gridviz-legend-category--active', false).select('svg').select('rect, circle').style('stroke', this.strokeColor),
|
|
56
|
+
})
|
|
51
57
|
//to enable vertical centering
|
|
52
58
|
//.style("position", "relative")
|
|
53
59
|
|
|
@@ -65,6 +71,7 @@ export class ColorCategoryLegend extends Legend {
|
|
|
65
71
|
.attr('height', h + 2 * sw)
|
|
66
72
|
|
|
67
73
|
.append('rect')
|
|
74
|
+
.classed('gridviz-legend-swatch', true)
|
|
68
75
|
.attr('x', sw)
|
|
69
76
|
.attr('y', sw)
|
|
70
77
|
.attr('width', w)
|
|
@@ -83,6 +90,7 @@ export class ColorCategoryLegend extends Legend {
|
|
|
83
90
|
.attr('height', h)
|
|
84
91
|
|
|
85
92
|
.append('circle')
|
|
93
|
+
.classed('gridviz-legend-swatch', true)
|
|
86
94
|
.attr('cx', r + sw)
|
|
87
95
|
.attr('cy', r + sw)
|
|
88
96
|
.attr('r', r)
|
|
@@ -94,16 +102,20 @@ export class ColorCategoryLegend extends Legend {
|
|
|
94
102
|
}
|
|
95
103
|
|
|
96
104
|
//write label text
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
105
|
+
this.setText(
|
|
106
|
+
d
|
|
107
|
+
.append('div')
|
|
108
|
+
//show on right of graphic
|
|
109
|
+
.style('display', 'inline')
|
|
100
110
|
|
|
101
|
-
|
|
102
|
-
|
|
111
|
+
//center vertically
|
|
112
|
+
//.style("position", "absolute").style("top", "0").style("bottom", "0")
|
|
103
113
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
114
|
+
.classed('gridviz-legend-label', true)
|
|
115
|
+
.style('padding-left', '5px')
|
|
116
|
+
.style('font-size', this.labelFontSizeCSS()),
|
|
117
|
+
cat[1]
|
|
118
|
+
)
|
|
107
119
|
}
|
|
108
120
|
}
|
|
109
121
|
}
|
|
@@ -21,8 +21,10 @@ export class ColorDiscreteLegend extends Legend {
|
|
|
21
21
|
/** @private @type {function(import('../core/Style').ViewScale):Array.<number>} */
|
|
22
22
|
this.breaks = opts.breaks
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
// a number of pixels, or 'fill' for the width of the container
|
|
25
|
+
this.width = opts.width === 'fill' ? 300 : opts.width || 300
|
|
25
26
|
this.height = opts.height || 15
|
|
27
|
+
this.setType('discrete')
|
|
26
28
|
|
|
27
29
|
this.tickSize = opts.tickSize || 3
|
|
28
30
|
|
|
@@ -48,27 +50,37 @@ export class ColorDiscreteLegend extends Legend {
|
|
|
48
50
|
//classes
|
|
49
51
|
const nb = colors.length
|
|
50
52
|
if (nb == 0) return
|
|
51
|
-
const
|
|
53
|
+
const width = this.availableWidth(this.width)
|
|
54
|
+
const w = width / nb
|
|
52
55
|
|
|
53
56
|
//make svg element
|
|
54
57
|
const svg = this.div
|
|
55
58
|
.append('svg')
|
|
56
|
-
.attr('width',
|
|
59
|
+
.attr('width', width)
|
|
57
60
|
.attr('height', this.height + this.tickSize + 2 + 10)
|
|
58
61
|
|
|
59
62
|
//draw graphic elements
|
|
60
63
|
for (let i = 0; i < nb; i++) {
|
|
61
|
-
svg
|
|
64
|
+
const rect = svg
|
|
65
|
+
.append('rect')
|
|
66
|
+
.classed('gridviz-legend-swatch', true)
|
|
62
67
|
.attr('x', i * w)
|
|
63
68
|
.attr('y', 0)
|
|
64
69
|
.attr('width', w)
|
|
65
70
|
.attr('height', this.height)
|
|
66
71
|
.style('fill', colors[i])
|
|
72
|
+
// hovering a class shows only its cells on the map
|
|
73
|
+
this.bindHover(rect, colors[i], {
|
|
74
|
+
// a presentation attribute, so that a stylesheet can restyle the active swatch
|
|
75
|
+
enter: () => rect.classed('gridviz-legend-swatch--active', true).attr('stroke', 'red'),
|
|
76
|
+
leave: () => rect.classed('gridviz-legend-swatch--active', false).attr('stroke', null),
|
|
77
|
+
})
|
|
67
78
|
}
|
|
68
79
|
|
|
69
80
|
//tick line
|
|
70
81
|
for (let i = 1; i < nb; i++) {
|
|
71
82
|
svg.append('line')
|
|
83
|
+
.classed('gridviz-legend-tick', true)
|
|
72
84
|
.attr('x1', w * i)
|
|
73
85
|
.attr('y1', 0)
|
|
74
86
|
.attr('x2', w * i)
|
|
@@ -84,10 +96,11 @@ export class ColorDiscreteLegend extends Legend {
|
|
|
84
96
|
|
|
85
97
|
//label
|
|
86
98
|
svg.append('text')
|
|
99
|
+
.classed('gridviz-legend-label', true)
|
|
87
100
|
.attr('id', 'ticklabel_' + i)
|
|
88
101
|
.attr('x', w * i)
|
|
89
102
|
.attr('y', this.height + this.tickSize + 2)
|
|
90
|
-
.style('font-size', this.
|
|
103
|
+
.style('font-size', this.labelFontSizeCSS())
|
|
91
104
|
//.style("font-weight", "bold")
|
|
92
105
|
//.style("font-family", "Arial")
|
|
93
106
|
.style('text-anchor', 'middle')
|
|
@@ -35,8 +35,10 @@ export class ColorLegend extends Legend {
|
|
|
35
35
|
this.fontSize = opts.fontSize || '0.8em'
|
|
36
36
|
this.invert = opts.invert
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
// a number of pixels, or 'fill' for the width of the container
|
|
39
|
+
this.width = opts.width === 'fill' ? 300 : opts.width || 300
|
|
39
40
|
this.height = opts.height || 15
|
|
41
|
+
this.setType('color')
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
/**
|
|
@@ -49,7 +51,8 @@ export class ColorLegend extends Legend {
|
|
|
49
51
|
//title
|
|
50
52
|
this.makeTitle()
|
|
51
53
|
|
|
52
|
-
const
|
|
54
|
+
const width = this.availableWidth(this.width)
|
|
55
|
+
const svgW = width + 2 * this.margin
|
|
53
56
|
const svgH = this.height + this.margin + this.tickSize + 10
|
|
54
57
|
const svg = this.div.append('svg').attr('width', svgW).attr('height', svgH)
|
|
55
58
|
// <rect width="300" height="100" style="fill:rgb(0,0,255);stroke-width:3;stroke:rgb(0,0,0)" />
|
|
@@ -57,13 +60,14 @@ export class ColorLegend extends Legend {
|
|
|
57
60
|
const g = svg.append('g').attr('transform', 'translate(' + this.margin + ' ' + 0 + ')')
|
|
58
61
|
|
|
59
62
|
//draw color bar
|
|
60
|
-
const w =
|
|
63
|
+
const w = width,
|
|
61
64
|
h = this.height
|
|
62
65
|
const step = 5
|
|
63
66
|
for (let i = 0; i < w; i += step) {
|
|
64
67
|
let t = i / (w - 1)
|
|
65
68
|
if (this.invert) t = 1 - t
|
|
66
69
|
g.append('rect')
|
|
70
|
+
.classed('gridviz-legend-swatch', true)
|
|
67
71
|
.attr('x', i)
|
|
68
72
|
.attr('y', 0)
|
|
69
73
|
.attr('width', step)
|
|
@@ -76,6 +80,7 @@ export class ColorLegend extends Legend {
|
|
|
76
80
|
|
|
77
81
|
//tick line
|
|
78
82
|
g.append('line')
|
|
83
|
+
.classed('gridviz-legend-tick', true)
|
|
79
84
|
.attr('x1', w * t)
|
|
80
85
|
.attr('y1', 0)
|
|
81
86
|
.attr('x2', w * t)
|
|
@@ -84,6 +89,7 @@ export class ColorLegend extends Legend {
|
|
|
84
89
|
|
|
85
90
|
//prepare tick label
|
|
86
91
|
g.append('text')
|
|
92
|
+
.classed('gridviz-legend-label', true)
|
|
87
93
|
.attr('id', 'ticklabel_' + i)
|
|
88
94
|
.attr('x', w * t)
|
|
89
95
|
.attr('y', h + this.tickSize + 2)
|