gridviz 3.0.36 → 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.
@@ -55,22 +55,43 @@ export class TiledGrid extends Dataset {
55
55
  */
56
56
  loadInfo() {
57
57
  if (!this.info && this.infoLoadingStatus === 'notLoaded') {
58
+ // marked before the request, so the redraws that happen while it's under way
59
+ // don't request it again
60
+ this.infoLoadingStatus = 'loading'
58
61
  ;(async () => {
59
62
  try {
60
63
  const data = await json(this.url + 'info.json')
61
64
  this.info = data
62
65
  this.resolution = data.resolutionGeo
63
66
  this.infoLoadingStatus = 'loaded'
64
- this.map.redraw()
67
+ // all the resolutions of a multi-resolution dataset load their info at
68
+ // once: only the one currently shown needs a redraw
69
+ if (this.isShown()) this.map.redraw()
65
70
  } catch (error) {
66
71
  //mark as failed
67
72
  this.infoLoadingStatus = 'failed'
68
73
  }
69
74
  })()
70
- } else if (this.infoLoadingStatus === 'loaded' || this.infoLoadingStatus === 'failed') this.map.redraw()
75
+ }
76
+ // (no redraw for a failed info.json: every redraw would ask again - an endless loop)
71
77
  return this
72
78
  }
73
79
 
80
+ /**
81
+ * True when a visible layer of the map shows this dataset at the current zoom.
82
+ * @returns {boolean}
83
+ */
84
+ isShown() {
85
+ if (!this.map || !this.map.layers) return false
86
+ const z = this.map.getZoom()
87
+ for (const lay of this.map.layers) {
88
+ if (lay.visible && !lay.visible(z)) continue
89
+ if (!lay.getDataset) continue
90
+ if (lay.getDataset(z) == this) return true
91
+ }
92
+ return false
93
+ }
94
+
74
95
  /**
75
96
  * Compute a tiling envelope from a geographical envelope.
76
97
  * This is the function to use to know which tiles to download for a geographical view.
@@ -197,19 +218,7 @@ export class TiledGrid extends Dataset {
197
218
  // Check if any visible layer depends on this dataset
198
219
  // check if redraw is really needed, that is if:
199
220
  // 1. the dataset belongs to a layer which is visible at the current zoom level
200
- let needsRedraw = false
201
- //go through the layers
202
- const z = this.map.getZoom()
203
- for (const lay of this.map.layers) {
204
- if (lay.visible && !lay.visible(z)) continue
205
- if (!lay.getDataset) continue
206
- if (lay.getDataset(z) != this) continue
207
- //found one layer. No need to seek more.
208
- needsRedraw = true
209
- break
210
- }
211
-
212
- if (!needsRedraw) return
221
+ if (!this.isShown()) return
213
222
 
214
223
  // Check if tile intersects the current view
215
224
  const env = this.map.updateExtentGeo()
@@ -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)
@@ -13,6 +13,7 @@ export class OrientationLegend extends Legend {
13
13
  /** @param {Object} opts */
14
14
  constructor(opts) {
15
15
  super(opts)
16
+ this.setType('orientation')
16
17
  opts = opts || {}
17
18
 
18
19
  //orientation
@@ -38,7 +39,7 @@ export class OrientationLegend extends Legend {
38
39
  //title
39
40
  this.makeTitle()
40
41
 
41
- const d = this.div.append('div')
42
+ const d = this.div.append('div').classed('gridviz-legend-category', true)
42
43
 
43
44
  //compute segment color, width and length
44
45
  const color = this.color(opts.resolution, opts.z, opts.viewScale)
@@ -63,9 +64,10 @@ export class OrientationLegend extends Legend {
63
64
 
64
65
  //label
65
66
  d.append('div')
67
+ .classed('gridviz-legend-label', true)
66
68
  .style('display', 'inline')
67
69
  .style('padding-left', '5px')
68
- .style('font-size', this.labelFontSize)
70
+ .style('font-size', this.labelFontSizeCSS())
69
71
  .text(this.label + (this.labelUnitText ? ' ' : '') + this.labelUnitText)
70
72
  }
71
73
  }
@@ -15,6 +15,7 @@ export class SizeLegend extends Legend {
15
15
  /** @param {Object} opts */
16
16
  constructor(opts) {
17
17
  super(opts)
18
+ this.setType('size')
18
19
  opts = opts || {}
19
20
 
20
21
  /** A function returning the text label, from the view scale and list of cells, resolution and zoom
@@ -62,7 +63,7 @@ export class SizeLegend extends Legend {
62
63
  //format label, if specified and possible
63
64
  if (this.labelFormat && !isNaN(+label)) label = this.labelFormat(label)
64
65
 
65
- const d = this.div.append('div')
66
+ const d = this.div.append('div').classed('gridviz-legend-category', true)
66
67
  //to enable vertical centering
67
68
  //.style("position", "relative")
68
69
 
@@ -127,9 +128,10 @@ export class SizeLegend extends Legend {
127
128
 
128
129
  //label
129
130
  d.append('div')
131
+ .classed('gridviz-legend-label', true)
130
132
  .style('display', 'inline')
131
133
  .style('padding-left', '5px')
132
- .style('font-size', this.labelFontSize)
134
+ .style('font-size', this.labelFontSizeCSS())
133
135
  .text(label + (this.labelUnitText ? ' ' : '') + this.labelUnitText)
134
136
  }
135
137
  }
@@ -28,6 +28,7 @@ export class TernaryLegend extends Legend {
28
28
  this.rightText = opts.rightText || 'Category 2'
29
29
 
30
30
  this.centerCoefficient = opts.centerCoefficient || this.classifier.centerCoefficient
31
+ this.setType('ternary')
31
32
  }
32
33
 
33
34
  /**
@@ -51,6 +52,7 @@ export class TernaryLegend extends Legend {
51
52
 
52
53
  const padding = 2
53
54
  const fontSize = 12
55
+ const legend = this
54
56
 
55
57
  //make svg element
56
58
  const svg = this.div
@@ -62,6 +64,7 @@ export class TernaryLegend extends Legend {
62
64
  svg.append('text')
63
65
  .attr('x', w / 2)
64
66
  .attr('y', padding + fontSize)
67
+ .classed('gridviz-legend-label', true)
65
68
  .text(this.topText)
66
69
  .attr('font-size', fontSize)
67
70
  .attr('text-anchor', 'middle')
@@ -69,6 +72,7 @@ export class TernaryLegend extends Legend {
69
72
  svg.append('text')
70
73
  .attr('x', 0)
71
74
  .attr('y', 3 * padding + 2 * fontSize + h)
75
+ .classed('gridviz-legend-label', true)
72
76
  .text(this.leftText)
73
77
  .attr('font-size', fontSize)
74
78
  .attr('text-anchor', 'start')
@@ -76,6 +80,7 @@ export class TernaryLegend extends Legend {
76
80
  svg.append('text')
77
81
  .attr('x', w)
78
82
  .attr('y', 3 * padding + 2 * fontSize + h)
83
+ .classed('gridviz-legend-label', true)
79
84
  .text(this.rightText)
80
85
  .attr('font-size', fontSize)
81
86
  .attr('text-anchor', 'end')
@@ -96,12 +101,16 @@ export class TernaryLegend extends Legend {
96
101
  const setAttributes = (elt, color, text) => {
97
102
  //elt.raise();
98
103
  elt.attr('fill', color)
104
+ .classed('gridviz-legend-swatch', true)
99
105
  //.attr("stroke", colorOver)
100
106
  //.attr("stroke-width", 0)
101
107
  //.attr("stroke-linejoin", "round")
108
+ .style('cursor', this.interactive ? 'pointer' : null)
102
109
  .on('mouseover', function (e) {
103
110
  /*this.parentNode.appendChild(this); select(this).attr("stroke-width", selectionStrokeWidth);*/
104
111
  select(this).attr('fill', selectionColor)
112
+ // show only the cells of this class on the map
113
+ legend.highlight([color])
105
114
  if (!tt || !text) return
106
115
  tt.html(text)
107
116
  tt.setPosition(e)
@@ -110,6 +119,7 @@ export class TernaryLegend extends Legend {
110
119
  .on('mouseout', function () {
111
120
  /*select(this).attr("stroke-width", 0);*/
112
121
  select(this).attr('fill', color)
122
+ legend.highlight(null)
113
123
  if (tt) tt.hide()
114
124
  })
115
125
  if (tt && text)
@@ -3,7 +3,7 @@
3
3
 
4
4
  import { Style } from '../core/Style.js'
5
5
  import { randomNormal } from 'd3-random'
6
- import { checkWebGLSupport, makeWebGLCanvas } from '../utils/webGLUtils.js'
6
+ import { checkWebGLSupport, getSharedWebGLCanvas } from '../utils/webGLUtils.js'
7
7
  import { WebGLSquareColoring } from '../utils/WebGLSquareColoring.js'
8
8
  import { color } from 'd3-color'
9
9
 
@@ -62,8 +62,8 @@ export class DotDensityStyle extends Style {
62
62
  const ctx = geoCanvas.offscreenCtx
63
63
 
64
64
  if (checkWebGLSupport()) {
65
- //create canvas and webgl renderer
66
- const cvWGL = makeWebGLCanvas(geoCanvas.w + '', geoCanvas.h + '')
65
+ //the shared webgl canvas (one context for all styles, instead of a new one per draw)
66
+ const cvWGL = getSharedWebGLCanvas(geoCanvas.w, geoCanvas.h)
67
67
  if (!cvWGL) {
68
68
  console.error('No webGL')
69
69
  return
@@ -45,6 +45,9 @@ export class ShapeColorSizeStyle extends Style {
45
45
 
46
46
  const r2 = resolution * 0.5
47
47
 
48
+ // the legend swatch hovered, if any: only the cells of its colour are drawn
49
+ const highlight = this.highlightColors
50
+
48
51
  // Precompute if color, size, and shape are functions, for efficiency
49
52
  const isColorFunction = typeof this.color === 'function'
50
53
  const isSizeFunction = typeof this.size === 'function'
@@ -64,6 +67,7 @@ export class ShapeColorSizeStyle extends Style {
64
67
  //@ts-ignore
65
68
  const col = colorFunction ? colorFunction(c, resolution, z, viewScale) : defaultColor
66
69
  if (!col || col === 'none') continue
70
+ if (highlight && !this.isHighlighted(col)) continue
67
71
 
68
72
  // Determine size
69
73
  //@ts-ignore