@uwdata/mosaic-plot 0.10.0 → 0.12.0

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.
@@ -0,0 +1,267 @@
1
+ import { parsePath } from './parse-path.js';
2
+
3
+ /**
4
+ * Return SVG elements that intersect the provided spatial extent.
5
+ * @param {SVGSVGElement} svg Parent SVG element to search within.
6
+ * @param {SVGElement} el SVG container element to search for points.
7
+ * @param {[[number, number], [number, number]]} extent Rectangular brush
8
+ * extent within which to select elements. The extent is represented as
9
+ * upper-left and bottom-right (x, y) coordinates.
10
+ * @returns {Element[]} A list of intersecting SVG elements.
11
+ */
12
+ export function intersect(svg, el, extent) {
13
+ // svg origin in viewport coordinates
14
+ const { x, y } = svg.getBoundingClientRect();
15
+ const identity = svg.createSVGMatrix();
16
+
17
+ // collect elements that intersect the extent
18
+ const list = [];
19
+ for (const child of el.children) {
20
+ if (child.tagName === 'g') {
21
+ // handle faceted mark
22
+ const matrix = getTransformMatrix(child) ?? identity;
23
+ for (const grandchild of child.children) {
24
+ if (intersects(extent, x, y, grandchild, matrix)) {
25
+ list.push(grandchild);
26
+ }
27
+ }
28
+ } else if (intersects(extent, x, y, child, identity)) {
29
+ list.push(child);
30
+ }
31
+ }
32
+ return list;
33
+ }
34
+
35
+ function intersects(sel, ox, oy, el, matrix) {
36
+ let [[l, t], [r, b]] = sel;
37
+
38
+ // facet groups involve translation only
39
+ const { e: tx, f: ty } = matrix;
40
+
41
+ // getBoundingClientRect uses viewport coordinates
42
+ // so we first translate to SVG coordinates
43
+ const c = el.getBoundingClientRect();
44
+ const cl = c.left - ox;
45
+ const cr = c.right - ox;
46
+ const ct = c.top - oy;
47
+ const cb = c.bottom - oy;
48
+
49
+ if (cl >= l && cr <= r && ct >= t && cb <= b) {
50
+ // if selection encloses item bounds, we're done
51
+ return true;
52
+ } else if (cl <= r && cr >= l && ct <= b && cb >= t) {
53
+ // if selection intersects item bounds, test further
54
+ let tag = el.tagName;
55
+
56
+ // if a hyperlink, use enclosed element
57
+ if (tag === 'a') {
58
+ el = el.children[0];
59
+ tag = el.tagName;
60
+ }
61
+
62
+ // handle marks that rely solely on bbox intersection
63
+ if (tag === 'rect' || tag === 'text' || tag === 'image') {
64
+ return true;
65
+ }
66
+
67
+ // translate selection relative to enclosing group
68
+ l -= tx;
69
+ t -= ty;
70
+ r -= tx;
71
+ b -= ty;
72
+
73
+ switch (tag) {
74
+ case 'circle':
75
+ return intersectCircle(l, t, r, b, $(el.cx), $(el.cy), $(el.r));
76
+ case 'line':
77
+ return intersectLine(l, t, r, b, $(el.x1), $(el.y1), $(el.x2), $(el.y2));
78
+ case 'path':
79
+ return intersectPath(l, t, r, b, el);
80
+ }
81
+ }
82
+ return false;
83
+ }
84
+
85
+ function $(attr) {
86
+ return attr.baseVal.value;
87
+ }
88
+
89
+ function getTransformMatrix(el) {
90
+ const transform = el.transform.baseVal;
91
+ const n = transform.length;
92
+ let m = transform[0]?.matrix;
93
+ for (let i = 1; i < n; ++i) {
94
+ m = m.multiply(transform[i].matrix);
95
+ }
96
+ return m;
97
+ }
98
+
99
+ function intersectCircle(l, t, r, b, cx, cy, cr) {
100
+ const h = l <= cx && cx <= r;
101
+ const v = t <= cy && cy <= b;
102
+ if (h && v) return true; // center is enclosed
103
+
104
+ const dx = Math.min(Math.abs(l - cx), Math.abs(r - cx));
105
+ if (v && dx <= cr) return true;
106
+
107
+ const dy = Math.min(Math.abs(t - cy), Math.abs(b - cy));
108
+ return (h && dy <= cr) || (dx * dx + dy * dy <= cr * cr);
109
+ }
110
+
111
+ function intersectLine(l, t, r, b, x1, y1, x2, y2) {
112
+ const xmin = Math.max(Math.min(x1, x2), l);
113
+ const xmax = Math.min(Math.max(x1, x2), r);
114
+ if (xmin > xmax) return false;
115
+ let yl1 = y1;
116
+ let yl2 = y2;
117
+ const dx = x2 - x1;
118
+ if (Math.abs(dx) > 1e-8) {
119
+ const a = (y2 - y1) / dx;
120
+ const b = y1 - a * x1;
121
+ yl1 = a * xmin + b;
122
+ yl2 = a * xmax + b;
123
+ }
124
+ const ymin = Math.max(Math.min(yl1, yl2), t);
125
+ const ymax = Math.min(Math.max(yl1, yl2), b);
126
+ return ymin <= ymax;
127
+ }
128
+
129
+ export function intersectPath(l, t, r, b, el) {
130
+ // parse path and cache result for reuse
131
+ const cmds = el.__path__ || (el.__path__ = parsePath(el.getAttribute('d')));
132
+
133
+ let anchorX = 0;
134
+ let anchorY = 0;
135
+ let x = 0;
136
+ let y = 0;
137
+ let hit = false;
138
+ let poly = [0, 0];
139
+ let n = 2;
140
+
141
+ const matrix = getTransformMatrix(el);
142
+
143
+ const setAnchor = (ax, ay) => {
144
+ poly.length = n = 2;
145
+ poly[0] = x = anchorX = ax;
146
+ poly[1] = y = anchorY = ay;
147
+ };
148
+
149
+ const anchor = matrix
150
+ ? (x, y) => setAnchor(multiplyX(matrix, x, y), multiplyY(matrix, x, y))
151
+ : (x, y) => setAnchor(x, y);
152
+
153
+ const test = (x2, y2) => {
154
+ poly[n] = x2;
155
+ poly[n+1] = y2;
156
+ n += 2;
157
+ return intersectLine(l, t, r, b, poly[n-4], poly[n-3], x2, y2);
158
+ }
159
+
160
+ const lineTo = matrix
161
+ ? (x2, y2) => {
162
+ hit = test(
163
+ multiplyX(matrix, x = x2, y = y2),
164
+ multiplyY(matrix, x2, y2)
165
+ );
166
+ }
167
+ : (x2, y2) => { hit = test(x = x2, y = y2); };
168
+
169
+ for (let i = 0; i < cmds.length; ++i) {
170
+ const cmd = cmds[i];
171
+ switch (cmd[0]) {
172
+ case 'M': anchor(cmd[1], cmd[2]); break;
173
+ case 'm': anchor(x + cmd[1], y + cmd[2]); break;
174
+ case 'L':
175
+ case 'T': lineTo(cmd[1], cmd[2]); break;
176
+ case 'H': lineTo(cmd[1], y); break;
177
+ case 'V': lineTo(x, cmd[1]); break;
178
+ case 'l':
179
+ case 't': lineTo(x + cmd[1], y + cmd[2]); break;
180
+ case 'h': lineTo(x + cmd[1], y); break;
181
+ case 'v': lineTo(x, y + cmd[1]); break;
182
+
183
+ // approximate bezier curve as line for now
184
+ case 'C': lineTo(cmd[5], cmd[6]); break;
185
+ case 'c': lineTo(x + cmd[5], y + cmd[6]); break;
186
+ case 'S':
187
+ case 'Q': lineTo(cmd[3], cmd[4]); break;
188
+ case 's':
189
+ case 'q': lineTo(x + cmd[3], y + cmd[4]); break;
190
+
191
+ // we don't expect to see arcs other than geo point circles
192
+ // but just in case, approximate via straight line for now
193
+ case 'A': lineTo(cmd[6], cmd[7]); break;
194
+ case 'a':
195
+ if (isCircle(cmds, i)) {
196
+ // special case for geo point circle
197
+ return intersectCircle(l, t, r, b, x, y - cmd[2], cmd[2]);
198
+ } else {
199
+ lineTo(x + cmd[6], x + cmd[7]);
200
+ }
201
+ break;
202
+
203
+ case 'z':
204
+ case 'Z':
205
+ lineTo(anchorX, anchorY);
206
+ if (pointInPolygon(l, t, poly) > 0) return true;
207
+ anchor(anchorX, anchorY);
208
+ break;
209
+ default:
210
+ // bail for now
211
+ console.warn('SVG path command not supported: ', cmd[0]);
212
+ return false;
213
+ }
214
+ if (hit) return true;
215
+ }
216
+ return false;
217
+ }
218
+
219
+ function multiplyX(m, x, y) {
220
+ return m.a * x + m.c * y + m.e;
221
+ }
222
+
223
+ function multiplyY(m, x, y) {
224
+ return m.b * x + m.d * y + m.f;
225
+ }
226
+
227
+ function isCircle(cmds, i) {
228
+ const a = cmds[i];
229
+ const b = cmds[i+1];
230
+ return b && b[0] === 'a'
231
+ && cmds[i+2]?.[0] === 'z'
232
+ && a[1] === a[2]
233
+ && b[1] === b[2]
234
+ && a[1] === b[1]
235
+ && a[7] === -b[7];
236
+ }
237
+
238
+ /**
239
+ * Point in polygon test, based on Dan Sunday's winding number algorithm.
240
+ * https://web.archive.org/web/20130126163405/http://geomalgorithms.com/a03-_inclusion.html
241
+ * @param {number} x The x-coordinate to test for inclusion
242
+ * @param {number} y The y-coordinate to test for inclusion
243
+ * @param {number[]} poly Polygon vertices as a flat array of numbers
244
+ * @returns {number} The winding number. Non-zero values indicate inclusion.
245
+ */
246
+ function pointInPolygon(x, y, poly) {
247
+ let wn = 0;
248
+ const n = poly.length - 2;
249
+
250
+ for (let i = 0; i < n; i += 2) {
251
+ if (poly[i + 1] <= y) {
252
+ // an upward crossing and (x,y) left of edge
253
+ if (poly[i + 3] > y && isLeft(x, y, poly, i) > 0)
254
+ ++wn; // valid up intersect
255
+ }
256
+ // a downward crossing and (x,y) right of edge
257
+ else if (poly[i + 3] <= y && isLeft(x, y, poly[i]) < 0) {
258
+ --wn; // valid down intersect
259
+ }
260
+ }
261
+
262
+ return wn;
263
+ }
264
+
265
+ function isLeft(x, y, p, i) {
266
+ return (p[i+2] - p[i]) * (y - p[i+1]) - (x - p[i]) * (p[i+3] - p[i+1]);
267
+ }
@@ -0,0 +1,14 @@
1
+ export function neqSome(a, b) {
2
+ return (a == null || b == null)
3
+ ? (a != null || b != null)
4
+ : (a.length !== b.length || a.some((x, i) => neq(x, b[i])));
5
+ }
6
+
7
+ export function neq(a, b) {
8
+ const n = a.length;
9
+ if (b.length !== n) return true;
10
+ for (let i = 0; i < n; ++i) {
11
+ if (a[i] !== b[i]) return true;
12
+ }
13
+ return false;
14
+ }
@@ -0,0 +1,79 @@
1
+ const paramCounts = { m:2, l:2, h:1, v:1, z:0, c:6, s:4, q:4, t:2, a:7 };
2
+ const commandPattern = /[mlhvzcsqta]([^mlhvzcsqta]+|$)/gi;
3
+ const numberPattern = /^[+-]?(([0-9]*\.[0-9]+)|([0-9]+\.)|([0-9]+))([eE][+-]?[0-9]+)?/;
4
+ const spacePattern = /^((\s+,?\s*)|(,\s*))/;
5
+ const flagPattern = /^[01]/;
6
+
7
+ const errmsg = attr => `Invalid SVG path, incorrect parameter ${attr}`;
8
+
9
+ /**
10
+ * Parse an SVG path into a list of drawing commands.
11
+ * @param {string} path The SVG path string to parse
12
+ * @returns {[string, ...number][]} A list of drawing commands.
13
+ * Each command has a single letter as the first entry. All subsequent
14
+ * entries are numeric parameter values.
15
+ */
16
+ export function parsePath(path) {
17
+ const commands = [];
18
+ const matches = path.match(commandPattern) || [];
19
+
20
+ matches.forEach(str => {
21
+ let cmd = str[0];
22
+ const type = cmd.toLowerCase();
23
+
24
+ // parse parameters
25
+ const paramCount = paramCounts[type];
26
+ const params = parseParams(type, paramCount, str.slice(1).trim());
27
+ const count = params.length;
28
+
29
+ // error checking based on parameter count
30
+ if (count < paramCount || (count && count % paramCount !== 0)) {
31
+ throw new Error(errmsg('count'));
32
+ }
33
+
34
+ // register the command
35
+ commands.push([cmd, ...params.slice(0, paramCount)]);
36
+
37
+ // exit now if we're done, also handles zero-param 'z'
38
+ if (count === paramCount) {
39
+ return;
40
+ }
41
+
42
+ // handle implicit line-to
43
+ if (type === 'm') {
44
+ cmd = (cmd === 'M') ? 'L' : 'l';
45
+ }
46
+
47
+ // repeat command when given extended param list
48
+ for (let i = paramCount; i < count; i += paramCount) {
49
+ commands.push([cmd, ...params.slice(i, i + paramCount)]);
50
+ }
51
+ });
52
+
53
+ return commands;
54
+ }
55
+
56
+ function parseParams(type, paramCount, segment) {
57
+ const params = [];
58
+
59
+ for (let index = 0; paramCount && index < segment.length; ) {
60
+ for (let i = 0; i < paramCount; ++i) {
61
+ const pattern = type === 'a' && (i === 3 || i === 4) ? flagPattern : numberPattern;
62
+ const match = segment.slice(index).match(pattern);
63
+
64
+ if (match === null) {
65
+ throw new Error(errmsg('type'));
66
+ }
67
+
68
+ index += match[0].length;
69
+ params.push(+match[0]);
70
+
71
+ const ws = segment.slice(index).match(spacePattern);
72
+ if (ws !== null) {
73
+ index += ws[0].length;
74
+ }
75
+ }
76
+ }
77
+
78
+ return params;
79
+ }
@@ -1,4 +1,4 @@
1
- import { Query, argmax, argmin, max, min, sql } from '@uwdata/mosaic-sql';
1
+ import { m4 } from '@uwdata/mosaic-sql';
2
2
  import { binExpr } from './util/bin-expr.js';
3
3
  import { filteredExtent } from './util/extent.js';
4
4
  import { Mark } from './Mark.js';
@@ -6,7 +6,7 @@ import { Mark } from './Mark.js';
6
6
  export class ConnectedMark extends Mark {
7
7
  constructor(type, source, encodings) {
8
8
  const dim = type.endsWith('X') ? 'y' : type.endsWith('Y') ? 'x' : null;
9
- const req = dim ? { [dim]: ['min', 'max'] } : undefined;
9
+ const req = dim ? { [dim]: ['count', 'min', 'max'] } : undefined;
10
10
  super(type, source, encodings, req);
11
11
  this.dim = dim;
12
12
  }
@@ -18,23 +18,24 @@ export class ConnectedMark extends Mark {
18
18
  */
19
19
  query(filter = []) {
20
20
  const { plot, dim, source } = this;
21
- const { optimize = true } = source.options || {};
21
+ let optimize = source.options?.optimize;
22
22
  const q = super.query(filter);
23
23
  if (!dim) return q;
24
24
 
25
25
  const ortho = dim === 'x' ? 'y' : 'x';
26
26
  const value = this.channelField(ortho, { exact: true })?.as;
27
- const { field, as, type, min, max } = this.channelField(dim);
27
+ const { field, as, type, count, min, max } = this.channelField(dim);
28
28
  const isContinuous = type === 'date' || type === 'number';
29
29
 
30
+ const size = dim === 'x' ? plot.innerWidth() : plot.innerHeight();
31
+ optimize ??= (count / size) > 10; // threshold for applying M4
32
+
30
33
  if (optimize && isContinuous && value) {
31
- // TODO: handle stacked data!
32
- const size = dim === 'x' ? plot.innerWidth() : plot.innerHeight();
34
+ // TODO: handle stacked data
33
35
  const [lo, hi] = filteredExtent(filter, field) || [min, max];
34
36
  const [expr] = binExpr(this, dim, size, [lo, hi], 1, as);
35
- const cols = q.select()
36
- // @ts-ignore
37
- .map(c => c.as)
37
+ const cols = q._select
38
+ .map(c => c.alias)
38
39
  .filter(c => c !== as && c !== value);
39
40
  return m4(q, expr, as, value, cols);
40
41
  } else {
@@ -42,27 +43,3 @@ export class ConnectedMark extends Mark {
42
43
  }
43
44
  }
44
45
  }
45
-
46
- /**
47
- * M4 is an optimization for value-preserving time-series aggregation
48
- * (http://www.vldb.org/pvldb/vol7/p797-jugel.pdf). This implementation uses
49
- * an efficient version with a single scan and the aggregate function
50
- * argmin and argmax, following https://arxiv.org/pdf/2306.03714.pdf.
51
- */
52
- function m4(input, bin, x, y, cols = []) {
53
- const pixel = sql`FLOOR(${bin})::INTEGER`;
54
-
55
- const q = (sel) => Query
56
- .from(input)
57
- .select(sel)
58
- .groupby(pixel, cols);
59
-
60
- return Query
61
- .union(
62
- q([{ [x]: min(x), [y]: argmin(y, x) }, ...cols]),
63
- q([{ [x]: max(x), [y]: argmax(y, x) }, ...cols]),
64
- q([{ [x]: argmin(x, y), [y]: min(y) }, ...cols]),
65
- q([{ [x]: argmax(x, y), [y]: max(y) }, ...cols])
66
- )
67
- .orderby(cols, x);
68
- }
@@ -1,4 +1,4 @@
1
- import { Query, and, count, isNull, isBetween, sql, sum } from '@uwdata/mosaic-sql';
1
+ import { Query, and, lineDensity } from '@uwdata/mosaic-sql';
2
2
  import { binExpr } from './util/bin-expr.js';
3
3
  import { extentX, extentY } from './util/extent.js';
4
4
  import { handleParam } from './util/handle-param.js';
@@ -16,13 +16,13 @@ export class DenseLineMark extends RasterMark {
16
16
  }
17
17
 
18
18
  query(filter = []) {
19
- const { channels, normalize, source, pad } = this;
19
+ const { channels, normalize, pad } = this;
20
20
  const [nx, ny] = this.bins = this.binDimensions();
21
21
  const [x] = binExpr(this, 'x', nx, extentX(this, filter), pad);
22
22
  const [y] = binExpr(this, 'y', ny, extentY(this, filter), pad);
23
23
 
24
24
  const q = Query
25
- .from(source.table)
25
+ .from(this.sourceTable())
26
26
  .where(stripXY(this, filter));
27
27
 
28
28
  this.aggr = ['density'];
@@ -52,101 +52,21 @@ function stripXY(mark, filter) {
52
52
  if (Array.isArray(filter) && !filter.length) return filter;
53
53
 
54
54
  // get column expressions for x and y encoding channels
55
- const { column: xc } = mark.channelField('x');
56
- const { column: yc } = mark.channelField('y');
55
+ const xc = mark.channelField('x').column;
56
+ const yc = mark.channelField('y').column;
57
57
 
58
58
  // test if a range predicate filters the x or y channels
59
59
  const test = p => {
60
- const col = `${p.field}`;
61
- return p.op !== 'BETWEEN' || col !== xc && col !== yc;
60
+ const col = `${p.expr}`;
61
+ return p.type !== 'BETWEEN' || (col !== xc && col !== yc);
62
62
  };
63
63
 
64
64
  // filter boolean 'and' operations
65
65
  const filterAnd = p => p.op === 'AND'
66
- ? and(p.children.filter(c => test(c)))
66
+ ? and(p.clauses.filter(c => test(c)))
67
67
  : p;
68
68
 
69
69
  return Array.isArray(filter)
70
70
  ? filter.filter(p => test(p)).map(p => filterAnd(p))
71
71
  : filterAnd(filter);
72
72
  }
73
-
74
- function lineDensity(
75
- q, x, y, z, xn, yn,
76
- groupby = [], normalize = true
77
- ) {
78
- // select x, y points binned to the grid
79
- q.select({
80
- x: sql`FLOOR(${x})::INTEGER`,
81
- y: sql`FLOOR(${y})::INTEGER`
82
- });
83
-
84
- // select line segment end point pairs
85
- const groups = groupby.concat(z);
86
- const pairPart = groups.length ? `PARTITION BY ${groups.join(', ')} ` : '';
87
- const pairs = Query
88
- .from(q)
89
- .select(groups, {
90
- x0: 'x',
91
- y0: 'y',
92
- dx: sql`(lead(x) OVER sw - x)`,
93
- dy: sql`(lead(y) OVER sw - y)`
94
- })
95
- .window({ sw: sql`${pairPart}ORDER BY x ASC` })
96
- .qualify(and(
97
- sql`(x0 < ${xn} OR x0 + dx < ${xn})`,
98
- sql`(y0 < ${yn} OR y0 + dy < ${yn})`,
99
- sql`(x0 > 0 OR x0 + dx > 0)`,
100
- sql`(y0 > 0 OR y0 + dy > 0)`
101
- ));
102
-
103
- // indices to join against for rasterization
104
- // generate the maximum number of indices needed
105
- const num = Query
106
- .select({ x: sql`GREATEST(MAX(ABS(dx)), MAX(ABS(dy)))` })
107
- .from('pairs');
108
- const indices = Query.select({ i: sql`UNNEST(range((${num})))::INTEGER` });
109
-
110
- // rasterize line segments
111
- const raster = Query.unionAll(
112
- Query
113
- .select(groups, {
114
- x: sql`x0 + i`,
115
- y: sql`y0 + ROUND(i * dy / dx::FLOAT)::INTEGER`
116
- })
117
- .from('pairs', 'indices')
118
- .where(sql`ABS(dy) <= ABS(dx) AND i < ABS(dx)`),
119
- Query
120
- .select(groups, {
121
- x: sql`x0 + ROUND(SIGN(dy) * i * dx / dy::FLOAT)::INTEGER`,
122
- y: sql`y0 + SIGN(dy) * i`
123
- })
124
- .from('pairs', 'indices')
125
- .where(sql`ABS(dy) > ABS(dx) AND i < ABS(dy)`),
126
- Query
127
- .select(groups, { x: 'x0', y: 'y0' })
128
- .from('pairs')
129
- .where(isNull('dx'))
130
- );
131
-
132
- // filter raster, normalize columns for each series
133
- const pointPart = ['x'].concat(groups).join(', ');
134
- const points = Query
135
- .from('raster')
136
- .select(groups, 'x', 'y',
137
- normalize
138
- ? { w: sql`1.0 / COUNT(*) OVER (PARTITION BY ${pointPart})` }
139
- : null
140
- )
141
- .where(and(isBetween('x', [0, xn], true), isBetween('y', [0, yn], true)));
142
-
143
- // sum normalized, rasterized series into output grids
144
- return Query
145
- .with({ pairs, indices, raster, points })
146
- .from('points')
147
- .select(groupby, {
148
- index: sql`x + y * ${xn}::INTEGER`,
149
- density: normalize ? sum('w') : count()
150
- })
151
- .groupby('index', groupby);
152
- }
@@ -1,5 +1,5 @@
1
1
  import { toDataColumns } from '@uwdata/mosaic-core';
2
- import { Query, gt, isBetween, sql, sum } from '@uwdata/mosaic-sql';
2
+ import { binLinear1d, isBetween } from '@uwdata/mosaic-sql';
3
3
  import { Transient } from '../symbols.js';
4
4
  import { binExpr } from './util/bin-expr.js';
5
5
  import { dericheConfig, dericheConv1d } from './util/density.js';
@@ -28,7 +28,7 @@ export class Density1DMark extends Mark {
28
28
  });
29
29
  }
30
30
 
31
- get filterIndexable() {
31
+ get filterStable() {
32
32
  const name = this.dim === 'x' ? 'xDomain' : 'yDomain';
33
33
  const dom = this.plot.getAttribute(name);
34
34
  return dom && !dom[Transient];
@@ -36,10 +36,10 @@ export class Density1DMark extends Mark {
36
36
 
37
37
  query(filter = []) {
38
38
  if (this.hasOwnData()) throw new Error('Density1DMark requires a data source');
39
- const { bins, channels, dim, source: { table } } = this;
39
+ const { bins, channels, dim } = this;
40
40
  const extent = this.extent = (dim === 'x' ? extentX : extentY)(this, filter);
41
41
  const [x, bx] = binExpr(this, dim, bins, extent);
42
- const q = markQuery(channels, table, [dim])
42
+ const q = markQuery(channels, this.sourceTable(), [dim])
43
43
  .where(filter.concat(isBetween(bx, extent)));
44
44
  const v = this.channelField('weight') ? 'weight' : null;
45
45
  return binLinear1d(q, x, v);
@@ -87,25 +87,3 @@ export class Density1DMark extends Mark {
87
87
  return [{ type, data: { length }, options }];
88
88
  }
89
89
  }
90
-
91
- function binLinear1d(q, p, density) {
92
- const w = density ? `* ${density}` : '';
93
-
94
- const u = q.clone().select({
95
- p,
96
- i: sql`FLOOR(p)::INTEGER`,
97
- w: sql`(FLOOR(p) + 1 - p)${w}`
98
- });
99
-
100
- const v = q.clone().select({
101
- p,
102
- i: sql`FLOOR(p)::INTEGER + 1`,
103
- w: sql`(p - FLOOR(p))${w}`
104
- });
105
-
106
- return Query
107
- .from(Query.unionAll(u, v))
108
- .select({ index: 'i', density: sum('w') })
109
- .groupby('index')
110
- .having(gt('density', 0));
111
- }
@@ -1,5 +1,5 @@
1
1
  import { toDataColumns } from '@uwdata/mosaic-core';
2
- import { avg, count, stddev } from '@uwdata/mosaic-sql';
2
+ import { avg, count, div, sqrt, stddev } from '@uwdata/mosaic-sql';
3
3
  import { erfinv } from './util/stats.js';
4
4
  import { Mark, markPlotSpec, markQuery } from './Mark.js';
5
5
  import { handleParam } from './util/handle-param.js';
@@ -20,13 +20,12 @@ export class ErrorBarMark extends Mark {
20
20
  }
21
21
 
22
22
  query(filter = []) {
23
- const { channels, field, source: { table } } = this;
23
+ const { channels, field } = this;
24
24
  const fields = channels.concat([
25
25
  { field: avg(field), as: '__avg__' },
26
- { field: count(field), as: '__n__', },
27
- { field: stddev(field), as: '__sd__' }
26
+ { field: div(stddev(field), sqrt(count(field))), as: '__se__', }
28
27
  ]);
29
- return markQuery(fields, table).where(filter);
28
+ return markQuery(fields, this.sourceTable()).where(filter);
30
29
  }
31
30
 
32
31
  queryResult(data) {
@@ -39,10 +38,10 @@ export class ErrorBarMark extends Mark {
39
38
 
40
39
  // compute confidence interval channels
41
40
  const p = Math.SQRT2 * erfinv(ci);
42
- const { columns: { __avg__: u, __sd__: s, __n__: n } } = data;
41
+ const { columns: { __avg__: u, __se__: s } } = data;
43
42
  const options = {
44
- [`${dim}1`]: u.map((u, i) => u - p * s[i] / Math.sqrt(n[i])),
45
- [`${dim}2`]: u.map((u, i) => u + p * s[i] / Math.sqrt(n[i]))
43
+ [`${dim}1`]: u.map((u, i) => u - p * s[i]),
44
+ [`${dim}2`]: u.map((u, i) => u + p * s[i])
46
45
  };
47
46
 
48
47
  return markPlotSpec(type, detail, channels, data, options);