gridviz 3.1.0 → 3.1.1

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.
Files changed (46) hide show
  1. package/README.md +18 -15
  2. package/dist/gridviz.js +3829 -2623
  3. package/dist/gridviz.min.js +1 -1
  4. package/package.json +5 -3
  5. package/src/button/Button.js +77 -68
  6. package/src/button/FullscreenButton.js +123 -123
  7. package/src/button/ZoomButtons.js +109 -104
  8. package/src/core/GeoCanvas.js +15 -0
  9. package/src/core/Legend.js +267 -245
  10. package/src/core/Map.js +819 -670
  11. package/src/core/Tooltip.js +191 -185
  12. package/src/index.js +82 -79
  13. package/src/layer/GridLayer.js +4 -3
  14. package/src/layer/LabelLayer.js +7 -4
  15. package/src/legend/ColorCategoryLegend.js +127 -125
  16. package/src/legend/ColorDiscreteLegend.js +116 -114
  17. package/src/legend/ColorLegend.js +33 -11
  18. package/src/legend/OrientationLegend.js +94 -92
  19. package/src/legend/SizeLegend.js +8 -1
  20. package/src/legend/TernaryLegend.js +317 -314
  21. package/src/legend/VennLegend.js +202 -0
  22. package/src/style/CircleColorSizeWebGLStyle.js +249 -0
  23. package/src/style/CompositionStyle.js +1 -1
  24. package/src/style/DotDensityStyle.js +8 -4
  25. package/src/style/ImageStyle.js +1 -1
  26. package/src/style/Interpolator.js +283 -259
  27. package/src/style/IsoFenceStyle.js +1 -1
  28. package/src/style/LegoStyle.js +6 -5
  29. package/src/style/MosaicStyle.js +1 -1
  30. package/src/style/NinjaStarStyle.js +1 -1
  31. package/src/style/PillarStyle.js +1 -1
  32. package/src/style/RandomWalkInterpolator.js +213 -0
  33. package/src/style/ShadingRayStyle.js +16 -2
  34. package/src/style/ShadingStyle.js +80 -72
  35. package/src/style/ShapeColorSizeStyle.js +18 -2
  36. package/src/style/SideCategoryStyle.js +1 -1
  37. package/src/style/SideStyle.js +1 -1
  38. package/src/style/SideTanakaStyle.js +1 -1
  39. package/src/style/SquareColorCategoryWebGLStyle.js +71 -20
  40. package/src/style/SquareColorWebGLStyle.js +7 -4
  41. package/src/style/StrokeStyle.js +1 -1
  42. package/src/style/TextStyle.js +1 -1
  43. package/src/style/TimeSeriesStyle.js +14 -7
  44. package/src/utils/WebGLSquareColoringAdvanced.js +47 -9
  45. package/src/utils/scale.js +20 -0
  46. package/src/utils/webGLUtils.js +52 -0
@@ -0,0 +1,202 @@
1
+ //@ts-check
2
+ 'use strict'
3
+
4
+ import { Legend } from '../core/Legend.js'
5
+
6
+ /**
7
+ * A legend for the colors of a map which shows two or three variables as overlapping layers, one color for each,
8
+ * blended by multiplication. It is a Venn diagram: a circle for each variable, in its color, and the colors of the overlaps are the mix of the colors
9
+ * of the circles.
10
+ *
11
+ * It is interactive, like the other legends: when the pointer is over a part of the diagram, only the cells drawn in the color(s) of the circles
12
+ * at this place are shown on the map. For a part of a single circle, it is the cells of its color. For an overlap, it is the cells of the colors
13
+ * of all the circles which overlap. Hovering the label of a circle shows the cells of its color.
14
+ *
15
+ * By default, all the styles of the layer the legend is in are highlighted, so the legend should be put on one of the styles of the layer,
16
+ * and the styles which draw the variables should be the ones of the layer. The `styles` option sets the styles to highlight otherwise.
17
+ *
18
+ * @example
19
+ * style.legends = [
20
+ * new gridviz.VennLegend({
21
+ * title: 'Crops',
22
+ * items: [
23
+ * { label: 'Fruit trees', color: 'rgb(0,255,255)' },
24
+ * { label: 'Olive groves', color: 'rgb(245,210,0)' },
25
+ * { label: 'Vineyards', color: 'rgb(255,20,147)' },
26
+ * ],
27
+ * }),
28
+ * ]
29
+ *
30
+ * @module legend
31
+ * @author Joseph Davies
32
+ */
33
+ export class VennLegend extends Legend {
34
+ /**
35
+ * @param {Object} opts The options of the legend (see Legend for the common ones: title, labelFontSize, labelFormat, interactive, className...).
36
+ * @param {Array.<{label:string, color:string}>} opts.items The two or three variables: the label shown beside its circle, and its color. The color should be the one the style which draws the variable uses (it is matched with it to highlight the cells).
37
+ * @param {number} [opts.radius] The radius of the circles, in pixels. Default: 45.
38
+ * @param {number} [opts.opacity] The opacity of the circles, within [0,1]. Default: 1.
39
+ * @param {Array.<import('../core/Style.js').Style>} [opts.styles] The styles to highlight, which draw the variables. Default: all the styles of the layer of the legend.
40
+ */
41
+ constructor(opts) {
42
+ super(opts)
43
+ opts = opts || {}
44
+
45
+ /**
46
+ * The variables: label and color.
47
+ * @type {Array.<{label:string, color:string}>} */
48
+ this.items = (opts.items || []).slice(0, 3)
49
+ if (this.items.length < 2) throw new Error('A Venn legend needs two or three items')
50
+
51
+ /** The radius of the circles, in pixels.
52
+ * @type {number} */
53
+ this.radius = opts.radius || 45
54
+
55
+ /** The opacity of the circles.
56
+ * @type {number} */
57
+ this.opacity = opts.opacity === undefined ? 1 : opts.opacity
58
+
59
+ /**
60
+ * The styles to highlight, if not the ones of the layer.
61
+ * @type {Array.<import('../core/Style.js').Style>|undefined} */
62
+ this.styles = opts.styles
63
+
64
+ /** The parts of the diagram being hovered, as a key. @private @type {string|null} */
65
+ this._hovered = null
66
+
67
+ this.setType('venn')
68
+ }
69
+
70
+ /**
71
+ * The styles to highlight: the ones given, else all the ones of the layer.
72
+ * @returns {Array.<import('../core/Style.js').Style>}
73
+ * @override
74
+ * @protected
75
+ */
76
+ highlightedStyles() {
77
+ if (this.styles) return this.styles
78
+ return this.layerStyles && this.layerStyles.length ? this.layerStyles : super.highlightedStyles()
79
+ }
80
+
81
+ /** The centers of the circles, relative to the center of the diagram. @private */
82
+ centers() {
83
+ const d = this.items.length === 2 ? 1.1 * this.radius : 1.3 * this.radius
84
+ return this.items.length === 2
85
+ ? [
86
+ [-d / 2, 0],
87
+ [d / 2, 0],
88
+ ]
89
+ : [
90
+ [-d / 2, 0],
91
+ [d / 2, 0],
92
+ [0, (d * Math.sqrt(3)) / 2],
93
+ ]
94
+ }
95
+
96
+ /**
97
+ * Draw the legend.
98
+ */
99
+ update() {
100
+ //clear
101
+ this.div.selectAll('*').remove()
102
+
103
+ //title
104
+ this.makeTitle()
105
+
106
+ const R = this.radius
107
+ const centers = this.centers()
108
+ const n = this.items.length
109
+
110
+ // the size of the labels, which stand on the sides of the circles, and below the third one
111
+ const fontSize = parseFloat(this.labelFontSize) || 14
112
+ const unit = /px$/.test(this.labelFontSize) || !isNaN(+this.labelFontSize) ? 1 : 16
113
+ const fontPx = fontSize * unit
114
+ const textWidth = (/** @type {string} */ label) => label.length * fontPx * 0.56
115
+ const gap = 7
116
+
117
+ // the position of the circles, in the svg: the labels of the first two are on the sides, the one of the third is below
118
+ const leftWidth = textWidth(this.items[0].label) + gap
119
+ const rightWidth = textWidth(this.items[1].label) + gap
120
+ const spread = centers[1][0] - centers[0][0]
121
+ const cx = centers.map((c) => leftWidth + R + (c[0] - centers[0][0]))
122
+ const cy = centers.map((c) => R + c[1])
123
+ const w = Math.ceil(leftWidth + 2 * R + spread + rightWidth)
124
+ const height = Math.ceil(n === 2 ? 2 * R : centers[2][1] + 2 * R + gap + fontPx)
125
+
126
+ const svg = this.div
127
+ .append('svg')
128
+ .classed('gridviz-legend-graphic', true)
129
+ .attr('width', w)
130
+ .attr('height', height)
131
+ // a description for the screen readers: the diagram is a picture, made of the variables it shows
132
+ .attr('role', 'img')
133
+ .attr('aria-label', (this.title ? this.title + ': ' : '') + this.items.map((item) => item.label).join(', '))
134
+ .style('overflow', 'visible')
135
+ // the circles blend with each other only, not with what is behind the legend
136
+ .style('isolation', 'isolate')
137
+
138
+ //the circles
139
+ const circles = this.items.map((item, i) =>
140
+ svg
141
+ .append('circle')
142
+ .classed('gridviz-legend-swatch', true)
143
+ .attr('cx', cx[i])
144
+ .attr('cy', cy[i])
145
+ .attr('r', R)
146
+ .style('fill', item.color)
147
+ .style('fill-opacity', this.opacity)
148
+ .style('mix-blend-mode', 'multiply')
149
+ .style('transition', 'opacity 0.1s')
150
+ )
151
+
152
+ //the labels: on the left of the first circle, on the right of the second one, below the third one
153
+ const labels = this.items.map((item, i) => {
154
+ const anchor = i === 0 ? 'end' : i === 1 ? 'start' : 'middle'
155
+ const x = i === 0 ? cx[0] - R - gap : i === 1 ? cx[1] + R + gap : cx[2]
156
+ const y = i === 2 ? cy[2] + R + fontPx : cy[i]
157
+ const t = svg
158
+ .append('text')
159
+ .classed('gridviz-legend-label', true)
160
+ .attr('x', x)
161
+ .attr('y', y)
162
+ .attr('text-anchor', anchor)
163
+ .attr('dominant-baseline', i === 2 ? 'auto' : 'central')
164
+ .style('font-size', this.labelFontSizeCSS())
165
+ .style('fill', this.labelColorCSS())
166
+ this.setText(t, item.label)
167
+ return t
168
+ })
169
+
170
+ if (!this.interactive) return
171
+
172
+ // The parts hovered: the circles under the pointer. The others are faded, and only the cells of the colors of the hovered circles are shown
173
+ const hover = (/** @type {number[]} */ hit) => {
174
+ const key = hit.join(',')
175
+ if (key === this._hovered) return
176
+ this._hovered = key || null
177
+ circles.forEach((c, i) =>
178
+ c
179
+ .classed('gridviz-legend-swatch--active', hit.includes(i))
180
+ .style('opacity', hit.length && !hit.includes(i) ? 0.25 : 1)
181
+ )
182
+ this.highlight(hit.length ? hit.map((i) => this.items[i].color) : null)
183
+ }
184
+
185
+ // over the diagram: the circles which contain the pointer
186
+ svg.style('cursor', 'pointer')
187
+ .on('mousemove', (/** @type {MouseEvent} */ event) => {
188
+ const box = /** @type {SVGSVGElement} */ (svg.node()).getBoundingClientRect()
189
+ const x = event.clientX - box.left
190
+ const y = event.clientY - box.top
191
+ const hit = []
192
+ for (let i = 0; i < n; i++) if (Math.hypot(x - cx[i], y - cy[i]) <= R) hit.push(i)
193
+ // not over a circle (over a label, for example): the label takes it
194
+ if (hit.length || !(/** @type {Element} */ (event.target).classList.contains('gridviz-legend-label')))
195
+ hover(hit)
196
+ })
197
+ .on('mouseleave', () => hover([]))
198
+
199
+ // over a label: the circle of the label
200
+ labels.forEach((t, i) => t.style('cursor', 'pointer').on('mouseenter', () => hover([i])))
201
+ }
202
+ }
@@ -0,0 +1,249 @@
1
+ //@ts-check
2
+ 'use strict'
3
+
4
+ import { color as parseColor } from 'd3-color'
5
+ import { Style } from '../core/Style.js'
6
+ import { getBuffer, getProgram, getSharedWebGLCanvas } from '../utils/webGLUtils.js'
7
+
8
+ // One instance per circle: the position of its center relative to the view center (geographical unit), its radius (idem), and its color.
9
+ const FLOATS_PER_CIRCLE = 7
10
+
11
+ const VERTEX_SHADER = `#version 300 es
12
+ layout(location = 0) in vec2 corner;
13
+ layout(location = 1) in vec3 circle;
14
+ layout(location = 2) in vec4 col;
15
+ uniform vec2 canvasSize;
16
+ uniform float z;
17
+ out vec4 vCol;
18
+ out vec2 vLocal;
19
+ out float vRadius;
20
+ void main() {
21
+ float radius = circle.z / z;
22
+ // the quad is a pixel bigger than the circle, for its anti-aliased edge
23
+ vec2 local = corner * (radius + 1.0);
24
+ vec2 pix = circle.xy / z + local;
25
+ gl_Position = vec4(pix * 2.0 / canvasSize, 0.0, 1.0);
26
+ vCol = col;
27
+ vLocal = local;
28
+ vRadius = radius;
29
+ }
30
+ `
31
+
32
+ const FRAGMENT_SHADER = `#version 300 es
33
+ precision mediump float;
34
+ in vec4 vCol;
35
+ in vec2 vLocal;
36
+ in float vRadius;
37
+ out vec4 outColor;
38
+ void main() {
39
+ float a = clamp(vRadius + 0.5 - length(vLocal), 0.0, 1.0) * vCol.a;
40
+ outColor = vec4(vCol.rgb * a, a);
41
+ }
42
+ `
43
+
44
+ /** The colors already parsed, to not parse a color string for each cell. @type {Map<string, Array<number>|null>} */
45
+ const parsedColors = new Map()
46
+ /** @param {string} c @returns {Array<number>|null} r, g, b, a within [0,1] */
47
+ const rgba = (c) => {
48
+ let v = parsedColors.get(c)
49
+ if (v === undefined) {
50
+ const p = parseColor(c)
51
+ v = p ? [p.r / 255, p.g / 255, p.b / 255, p.opacity] : null
52
+ if (parsedColors.size > 10000) parsedColors.clear()
53
+ parsedColors.set(c, v)
54
+ }
55
+ return v
56
+ }
57
+
58
+ /** The data of the circles, reused from draw to draw. */
59
+ let instances = new Float32Array(FLOATS_PER_CIRCLE * 4096)
60
+
61
+ /**
62
+ * A style which shows grid cells as circles, with a specific color and size, drawn with WebGL.
63
+ *
64
+ * It is made for maps with many circles. The circles of the style are drawn by the GPU at once, instead of one by one,
65
+ * and then copied on the map in a single operation. It makes a big difference when there are many circles, and when the style
66
+ * has a blend operation such as "multiply", which is then done once for the style and not for each circle.
67
+ *
68
+ * As a consequence, the circles of one style do not blend with each other: they are all drawn first, then blended with what is below.
69
+ * It is the same as ShapeColorSizeStyle (with the 'circle' shape) in all other aspects.
70
+ * It needs WebGL 2. Without it, the circles are drawn one by one.
71
+ *
72
+ * @example
73
+ * new gridviz.CircleColorSizeWebGLStyle({
74
+ * color: (cell) => (cell.value > 100 ? 'red' : 'blue'),
75
+ * size: (cell, resolution) => resolution * 0.5,
76
+ * blendOperation: () => 'multiply',
77
+ * })
78
+ *
79
+ * @module style
80
+ * @author Joseph Davies
81
+ */
82
+ export class CircleColorSizeWebGLStyle extends Style {
83
+ /**
84
+ * @param {object} opts The options of the style (see Style for the common ones: filter, offset, alpha, blendOperation, visible, viewScale).
85
+ * @param {(function(import('../core/Dataset.js').Cell, number, number, object):string) | string} [opts.color] The color of the circle of a cell: a CSS color, or a function of the cell, the resolution, the zoom level and the view scale. A cell without color is not drawn. Default: '#EA6BAC'.
86
+ * @param {(function(import('../core/Dataset.js').Cell, number, number, object):number) | number} [opts.size] The diameter of the circle of a cell, in geographical unit: a number, or a function of the cell, the resolution, the zoom level and the view scale. A cell without size (0) is not drawn. Default: the resolution.
87
+ */
88
+ constructor(opts) {
89
+ super(opts)
90
+ opts = opts || {}
91
+ /**
92
+ * The color of the circle of a cell.
93
+ * @type {(function(import('../core/Dataset.js').Cell, number, number, object):string) | string} */
94
+ this.color = opts.color || '#EA6BAC'
95
+
96
+ /**
97
+ * The diameter of the circle of a cell, in geographical unit. A cell without size (0) is not drawn.
98
+ * @type {(function(import('../core/Dataset.js').Cell, number, number, object):number) | number} */
99
+ this.size = opts.size || ((cell, resolution) => resolution)
100
+ }
101
+
102
+ /**
103
+ * @param {Array.<import("../core/Dataset.js").Cell>} cells
104
+ * @param {import("../core/GeoCanvas.js").GeoCanvas} geoCanvas
105
+ * @param {number} resolution
106
+ * @override
107
+ */
108
+ draw(cells, geoCanvas, resolution) {
109
+ //filter
110
+ if (this.filter) cells = cells.filter(this.filter)
111
+
112
+ const z = geoCanvas.view.z
113
+ const viewScale = this.viewScale ? this.viewScale(cells, resolution, z) : undefined
114
+ const r2 = resolution * 0.5
115
+
116
+ // the legend swatch hovered, if any: only the cells of its colour are drawn
117
+ const highlight = this.highlightColors
118
+
119
+ const colorFunction = typeof this.color === 'function' ? this.color : null
120
+ const sizeFunction = typeof this.size === 'function' ? this.size : null
121
+
122
+ //the circles are positioned relative to the center of the view, to keep the precision of the 32 bits floats
123
+ const vx = geoCanvas.view.x
124
+ const vy = geoCanvas.view.y
125
+
126
+ //fill the data of the circles
127
+ if (instances.length < FLOATS_PER_CIRCLE * cells.length)
128
+ instances = new Float32Array(
129
+ FLOATS_PER_CIRCLE * Math.max(cells.length, 2 * (instances.length / FLOATS_PER_CIRCLE))
130
+ )
131
+ let n = 0
132
+ for (const c of cells) {
133
+ //@ts-ignore
134
+ const col = colorFunction ? colorFunction(c, resolution, z, viewScale) : this.color
135
+ if (!col || col === 'none') continue
136
+ if (highlight && !this.isHighlighted(col)) continue
137
+
138
+ //@ts-ignore
139
+ const size = sizeFunction ? sizeFunction(c, resolution, z, viewScale) : this.size
140
+ if (!size) continue
141
+
142
+ const v = rgba(col)
143
+ if (!v) continue
144
+
145
+ const offset = this.offset(c, resolution, z)
146
+ const i = n * FLOATS_PER_CIRCLE
147
+ instances[i] = c.x + r2 + offset.dx - vx
148
+ instances[i + 1] = c.y + r2 + offset.dy - vy
149
+ instances[i + 2] = size * 0.5
150
+ instances[i + 3] = v[0]
151
+ instances[i + 4] = v[1]
152
+ instances[i + 5] = v[2]
153
+ instances[i + 6] = v[3]
154
+ n++
155
+ }
156
+
157
+ if (n > 0) {
158
+ if (!this.drawWebGL(geoCanvas, n, z)) this.drawCanvas(geoCanvas, n)
159
+ }
160
+
161
+ //update legends
162
+ this.updateLegends({ viewScale: viewScale, resolution: resolution, z: z, cells: cells })
163
+ }
164
+
165
+ /**
166
+ * Draw the circles with webGL, on the shared canvas, and copy it on the map.
167
+ * @param {import("../core/GeoCanvas.js").GeoCanvas} geoCanvas
168
+ * @param {number} n
169
+ * @param {number} z
170
+ * @returns {boolean} false if webGL 2 is not available.
171
+ * @private
172
+ */
173
+ drawWebGL(geoCanvas, n, z) {
174
+ let cv
175
+ try {
176
+ cv = getSharedWebGLCanvas(geoCanvas.w, geoCanvas.h, { version: 2 })
177
+ } catch (e) {
178
+ return false
179
+ }
180
+ const gl = /** @type {WebGL2RenderingContext} */ (/** @type {unknown} */ (cv.gl))
181
+ if (!gl.drawArraysInstanced) return false
182
+
183
+ const program = getProgram(gl, VERTEX_SHADER, FRAGMENT_SHADER)
184
+ gl.useProgram(program)
185
+ gl.uniform2f(gl.getUniformLocation(program, 'canvasSize'), geoCanvas.w, geoCanvas.h)
186
+ gl.uniform1f(gl.getUniformLocation(program, 'z'), z)
187
+
188
+ //the corners of the quad of a circle, the same for all circles
189
+ gl.bindBuffer(gl.ARRAY_BUFFER, getBuffer(gl, 'CircleColorSizeWebGLStyle.corner'))
190
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW)
191
+ gl.enableVertexAttribArray(0)
192
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0)
193
+ gl.vertexAttribDivisor(0, 0)
194
+
195
+ //the circles: one instance each
196
+ const stride = FLOATS_PER_CIRCLE * 4
197
+ gl.bindBuffer(gl.ARRAY_BUFFER, getBuffer(gl, 'CircleColorSizeWebGLStyle.circles'))
198
+ gl.bufferData(gl.ARRAY_BUFFER, instances.subarray(0, n * FLOATS_PER_CIRCLE), gl.DYNAMIC_DRAW)
199
+ gl.enableVertexAttribArray(1)
200
+ gl.vertexAttribPointer(1, 3, gl.FLOAT, false, stride, 0)
201
+ gl.vertexAttribDivisor(1, 1)
202
+ gl.enableVertexAttribArray(2)
203
+ gl.vertexAttribPointer(2, 4, gl.FLOAT, false, stride, 12)
204
+ gl.vertexAttribDivisor(2, 1)
205
+
206
+ gl.clearColor(0, 0, 0, 0)
207
+ gl.clear(gl.COLOR_BUFFER_BIT)
208
+ //colors are premultiplied by the fragment shader
209
+ gl.enable(gl.BLEND)
210
+ gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
211
+ gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, n)
212
+ gl.disable(gl.BLEND)
213
+
214
+ //leave the context as found
215
+ gl.vertexAttribDivisor(1, 0)
216
+ gl.vertexAttribDivisor(2, 0)
217
+ gl.disableVertexAttribArray(0)
218
+ gl.disableVertexAttribArray(1)
219
+ gl.disableVertexAttribArray(2)
220
+
221
+ //copy on the map. The alpha and blend operation of the style are the ones of the canvas, set by the layer.
222
+ geoCanvas.initCanvasTransform()
223
+ geoCanvas.offscreenCtx.drawImage(cv.canvas, 0, 0)
224
+ return true
225
+ }
226
+
227
+ /**
228
+ * Draw the circles one by one, without webGL.
229
+ * @param {import("../core/GeoCanvas.js").GeoCanvas} geoCanvas
230
+ * @param {number} n
231
+ * @private
232
+ */
233
+ drawCanvas(geoCanvas, n) {
234
+ const ctx = geoCanvas.offscreenCtx
235
+ const vx = geoCanvas.view.x
236
+ const vy = geoCanvas.view.y
237
+ geoCanvas.setCanvasTransform()
238
+ for (let k = 0; k < n; k++) {
239
+ const i = k * FLOATS_PER_CIRCLE
240
+ ctx.fillStyle =
241
+ 'rgba(' +
242
+ [255 * instances[i + 3], 255 * instances[i + 4], 255 * instances[i + 5], instances[i + 6]].join(',') +
243
+ ')'
244
+ ctx.beginPath()
245
+ ctx.arc(instances[i] + vx, instances[i + 1] + vy, instances[i + 2], 0, 2 * Math.PI)
246
+ ctx.fill()
247
+ }
248
+ }
249
+ }
@@ -286,6 +286,6 @@ export class CompositionStyle extends Style {
286
286
  }
287
287
 
288
288
  //update legends
289
- this.updateLegends({ style: this, resolution: resolution, z: z, viewScale: viewScale })
289
+ this.updateLegends({ style: this, resolution: resolution, z: z, viewScale: viewScale, cells: cells })
290
290
  }
291
291
  }
@@ -74,6 +74,10 @@ export class DotDensityStyle extends Style {
74
74
 
75
75
  const r2 = resolution / 2
76
76
 
77
+ //the positions are relative to the center of the view, see GeoCanvas.getWebGLTransformCentered
78
+ const vx = geoCanvas.view.x
79
+ const vy = geoCanvas.view.y
80
+
77
81
  for (let cell of cells) {
78
82
  //get color
79
83
  const col = this.color(cell, resolution, z, viewScale)
@@ -86,8 +90,8 @@ export class DotDensityStyle extends Style {
86
90
  const offset = this.offset(cell, resolution, z)
87
91
 
88
92
  //cell center
89
- const cx = cell.x + offset.dx + r2
90
- const cy = cell.y + offset.dy + r2
93
+ const cx = cell.x + offset.dx + r2 - vx
94
+ const cy = cell.y + offset.dy + r2 - vy
91
95
 
92
96
  //convert color
93
97
  const cc = color(col)
@@ -99,7 +103,7 @@ export class DotDensityStyle extends Style {
99
103
  }
100
104
 
101
105
  //draw
102
- prog.draw(geoCanvas.getWebGLTransform())
106
+ prog.draw(geoCanvas.getWebGLTransformCentered())
103
107
 
104
108
  //draw in canvas geo
105
109
  geoCanvas.initCanvasTransform()
@@ -128,6 +132,6 @@ export class DotDensityStyle extends Style {
128
132
  }
129
133
 
130
134
  //update legends
131
- this.updateLegends({ style: this, resolution: resolution, z: z, viewScale: viewScale })
135
+ this.updateLegends({ style: this, resolution: resolution, z: z, viewScale: viewScale, cells: cells })
132
136
  }
133
137
  }
@@ -95,6 +95,6 @@ export class ImageStyle extends Style {
95
95
  }
96
96
 
97
97
  //update legends
98
- this.updateLegends({ style: this, resolution: resolution, z: z, viewScale: viewScale })
98
+ this.updateLegends({ style: this, resolution: resolution, z: z, viewScale: viewScale, cells: cells })
99
99
  }
100
100
  }