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/package.json CHANGED
@@ -1,74 +1,74 @@
1
- {
2
- "name": "gridviz",
3
- "version": "3.0.37",
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
+ }
@@ -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.div
82
- .append('div')
83
- .style('font-size', this.titleFontSize)
84
- .style('font-weight', this.titleFontWeight)
85
- .style('margin-bottom', '7px')
86
- .text(this.title)
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
- if (this.legend) this.legend.selectAll('*').remove()
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()
@@ -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 addLegends(legendComp, lg) {
121
- if (Array.isArray(lg)) for (const lg_ of lg) this.addLegends(legendComp, lg_)
122
- else legendComp.node().append(lg.div.node())
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
- d.append('div')
98
- //show on right of graphic
99
- .style('display', 'inline')
105
+ this.setText(
106
+ d
107
+ .append('div')
108
+ //show on right of graphic
109
+ .style('display', 'inline')
100
110
 
101
- //center vertically
102
- //.style("position", "absolute").style("top", "0").style("bottom", "0")
111
+ //center vertically
112
+ //.style("position", "absolute").style("top", "0").style("bottom", "0")
103
113
 
104
- .style('padding-left', '5px')
105
- .style('font-size', this.labelFontSize)
106
- .text(cat[1])
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
- this.width = opts.width || 300
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 w = this.width / nb
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', this.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.append('rect')
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.labelFontSize)
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
- this.width = opts.width || 300
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 svgW = this.width + 2 * this.margin
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 = this.width,
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)