@uwdata/mosaic-core 0.9.0 → 0.10.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.
@@ -24,7 +24,10 @@ import { MosaicClient } from './MosaicClient.js';
24
24
  * cross-filtering contexts.
25
25
  * @returns {SelectionClause} The generated selection clause.
26
26
  */
27
- export function point(field, value, { source, clients = undefined }) {
27
+ export function clausePoint(field, value, {
28
+ source,
29
+ clients = source ? new Set([source]) : undefined
30
+ }) {
28
31
  /** @type {SQLExpression | null} */
29
32
  const predicate = value !== undefined
30
33
  ? isNotDistinct(field, literal(value))
@@ -50,7 +53,10 @@ export function point(field, value, { source, clients = undefined }) {
50
53
  * cross-filtering contexts.
51
54
  * @returns {SelectionClause} The generated selection clause.
52
55
  */
53
- export function points(fields, value, { source, clients = undefined }) {
56
+ export function clausePoints(fields, value, {
57
+ source,
58
+ clients = source ? new Set([source]) : undefined
59
+ }) {
54
60
  /** @type {SQLExpression | null} */
55
61
  let predicate = null;
56
62
  if (value) {
@@ -83,13 +89,17 @@ export function points(fields, value, { source, clients = undefined }) {
83
89
  * @param {number} [options.pixelSize=1] The interactive pixel size.
84
90
  * @returns {SelectionClause} The generated selection clause.
85
91
  */
86
- export function interval(field, value, {
87
- source, clients, bin, scale, pixelSize = 1
92
+ export function clauseInterval(field, value, {
93
+ source,
94
+ clients = source ? new Set([source]) : undefined,
95
+ bin,
96
+ scale,
97
+ pixelSize = 1
88
98
  }) {
89
99
  /** @type {SQLExpression | null} */
90
100
  const predicate = value != null ? isBetween(field, value) : null;
91
101
  /** @type {import('./util/selection-types.js').IntervalMetadata} */
92
- const meta = { type: 'interval', scales: [scale], bin, pixelSize };
102
+ const meta = { type: 'interval', scales: scale && [scale], bin, pixelSize };
93
103
  return { meta, source, clients, value, predicate };
94
104
  }
95
105
 
@@ -108,8 +118,12 @@ export function interval(field, value, {
108
118
  * @param {number} [options.pixelSize=1] The interactive pixel size.
109
119
  * @returns {SelectionClause} The generated selection clause.
110
120
  */
111
- export function intervals(fields, value, {
112
- source, clients, bin, scales = [], pixelSize = 1
121
+ export function clauseIntervals(fields, value, {
122
+ source,
123
+ clients = source ? new Set([source]) : undefined,
124
+ bin,
125
+ scales = [],
126
+ pixelSize = 1
113
127
  }) {
114
128
  /** @type {SQLExpression | null} */
115
129
  const predicate = value != null
@@ -135,7 +149,7 @@ const MATCH_METHODS = { contains, prefix, suffix, regexp: regexp_matches };
135
149
  * text matching method to use. Defaults to `'contains'`.
136
150
  * @returns {SelectionClause} The generated selection clause.
137
151
  */
138
- export function match(field, value, {
152
+ export function clauseMatch(field, value, {
139
153
  source, clients = undefined, method = 'contains'
140
154
  }) {
141
155
  let fn = MATCH_METHODS[method];
package/src/index.js CHANGED
@@ -3,18 +3,27 @@ export { Coordinator, coordinator } from './Coordinator.js';
3
3
  export { Selection, isSelection } from './Selection.js';
4
4
  export { Param, isParam } from './Param.js';
5
5
  export { Priority } from './QueryManager.js';
6
- export { point, points, interval, intervals, match } from './SelectionClause.js';
7
6
 
8
7
  export { restConnector } from './connectors/rest.js';
9
8
  export { socketConnector } from './connectors/socket.js';
10
9
  export { wasmConnector } from './connectors/wasm.js';
11
10
 
11
+ export {
12
+ clauseInterval,
13
+ clauseIntervals,
14
+ clausePoint,
15
+ clausePoints,
16
+ clauseMatch
17
+ } from './SelectionClause.js';
18
+
12
19
  export {
13
20
  isArrowTable,
14
21
  convertArrowArrayType,
15
22
  convertArrowValue,
16
23
  convertArrowColumn
17
24
  } from './util/convert-arrow.js'
25
+
18
26
  export { distinct } from './util/distinct.js';
19
27
  export { synchronizer } from './util/synchronizer.js';
20
28
  export { throttle } from './util/throttle.js';
29
+ export { toDataColumns } from './util/to-data-columns.js'
@@ -1,8 +1,7 @@
1
1
  /**
2
- * Event dispatcher supporting asynchronous updates.
3
- * If an event handler callback returns a Promise, this dispatcher will
4
- * wait for all such Promises to settle before dispatching future events
5
- * of the same type.
2
+ * Event dispatcher supporting asynchronous updates. If an event handler
3
+ * callback returns a Promise, the dispatcher waits for all such Promises
4
+ * to settle before dispatching future events of the same type.
6
5
  */
7
6
  export class AsyncDispatch {
8
7
 
@@ -63,7 +62,7 @@ export class AsyncDispatch {
63
62
  * queue of unemitted event values prior to enqueueing a new value.
64
63
  * This default implementation simply returns null, indicating that
65
64
  * any other unemitted event values should be dropped (that is, all
66
- * queued events are filtered)
65
+ * queued events are filtered).
67
66
  * @param {string} type The event type.
68
67
  * @param {*} value The new event value that will be enqueued.
69
68
  * @returns {(value: *) => boolean|null} A dispatch queue filter
@@ -83,6 +82,17 @@ export class AsyncDispatch {
83
82
  entry?.queue.clear();
84
83
  }
85
84
 
85
+ /**
86
+ * Returns a promise that resolves when any pending updates complete for
87
+ * the event of the given type currently being processed. The Promise will
88
+ * resolve immediately if the queue for the given event type is empty.
89
+ * @param {string} type The event type to wait for.
90
+ * @returns {Promise} A pending event promise.
91
+ */
92
+ async pending(type) {
93
+ await this._callbacks.get(type)?.pending;
94
+ }
95
+
86
96
  /**
87
97
  * Emit an event value to listeners for the given event type.
88
98
  * If a previous emit has not yet resolved, the event value
@@ -1,21 +1,20 @@
1
1
  import { Query, agg, sql } from '@uwdata/mosaic-sql';
2
2
  import { MosaicClient } from '../MosaicClient.js';
3
3
 
4
- export const NO_INDEX = { from: NaN };
5
-
6
4
  /**
7
5
  * Determine data cube index columns for a given Mosaic client.
8
6
  * @param {MosaicClient} client The Mosaic client.
9
7
  * @returns An object with necessary column data to generate data
10
- * cube index columns, null if an invalid or unsupported expression
11
- * is encountered, or NO_INDEX if the client is not indexable.
8
+ * cube index columns, or null if the client is not indexable or
9
+ * the client query contains an invalid or unsupported expression.
12
10
  */
13
11
  export function indexColumns(client) {
14
- if (!client.filterIndexable) return NO_INDEX;
12
+ if (!client.filterIndexable) return null;
15
13
  const q = client.query();
16
14
  const from = getBaseTable(q);
17
- if (typeof from !== 'string' || !q.groupby) return NO_INDEX;
18
- const g = new Set(q.groupby().map(c => c.column));
15
+
16
+ // bail if no base table or the query is not analyzable
17
+ if (typeof from !== 'string' || !q.select) return null;
19
18
 
20
19
  const aggr = []; // list of output aggregate columns
21
20
  const dims = []; // list of grouping dimension columns
@@ -127,11 +126,14 @@ export function indexColumns(client) {
127
126
 
128
127
  // otherwise, check if dimension
129
128
  default:
130
- if (g.has(as)) dims.push(as);
129
+ if (!aggregate) dims.push(as);
131
130
  else return null; // unsupported aggregate
132
131
  }
133
132
  }
134
133
 
134
+ // bail if the query has no aggregates
135
+ if (!aggr.length) return null;
136
+
135
137
  return { from, dims, aggr, aux };
136
138
  }
137
139
 
@@ -1,9 +1,42 @@
1
- export function queryResult() {
2
- let resolve;
3
- let reject;
4
- const p = new Promise((r, e) => { resolve = r; reject = e; });
5
- return Object.assign(p, {
6
- fulfill: value => (resolve(value), p),
7
- reject: err => (reject(err), p)
8
- });
1
+ /**
2
+ * A query result Promise that can allows external callers
3
+ * to resolve or reject the Promise.
4
+ */
5
+ export class QueryResult extends Promise {
6
+ /**
7
+ * Create a new query result Promise.
8
+ */
9
+ constructor() {
10
+ let resolve;
11
+ let reject;
12
+ super((r, e) => {
13
+ resolve = r;
14
+ reject = e;
15
+ });
16
+ this._resolve = resolve;
17
+ this._reject = reject;
18
+ }
19
+
20
+ /**
21
+ * Resolve the result Promise with the provided value.
22
+ * @param {*} value The result value.
23
+ * @returns {this}
24
+ */
25
+ fulfill(value) {
26
+ this._resolve(value);
27
+ return this;
28
+ }
29
+
30
+ /**
31
+ * Rejects the result Promise with the provided error.
32
+ * @param {*} error The error value.
33
+ * @returns {this}
34
+ */
35
+ reject(error) {
36
+ this._reject(error);
37
+ return this;
38
+ }
9
39
  }
40
+
41
+ // necessary to make Promise subclass act like a Promise
42
+ QueryResult.prototype.constructor = Promise;
@@ -0,0 +1,71 @@
1
+ import { convertArrowColumn, isArrowTable } from './convert-arrow.js';
2
+
3
+ /**
4
+ * @typedef {Array | Int8Array | Uint8Array | Uint8ClampedArray
5
+ * | Int16Array | Uint16Array | Int32Array | Uint32Array
6
+ * | Float32Array | Float64Array
7
+ * } Arrayish - an Array or TypedArray
8
+ */
9
+
10
+ /**
11
+ * @typedef {
12
+ * | { numRows: number, columns: Record<string,Arrayish> }
13
+ * | { numRows: number, values: Arrayish; }
14
+ * } DataColumns
15
+ */
16
+
17
+ /**
18
+ * Convert input data to a set of column arrays.
19
+ * @param {any} data The input data.
20
+ * @returns {DataColumns} An object with named column arrays.
21
+ */
22
+ export function toDataColumns(data) {
23
+ return isArrowTable(data)
24
+ ? arrowToColumns(data)
25
+ : arrayToColumns(data);
26
+ }
27
+
28
+ /**
29
+ * Convert an Arrow table to a set of column arrays.
30
+ * @param {import('apache-arrow').Table} data An Apache Arrow Table.
31
+ * @returns {DataColumns} An object with named column arrays.
32
+ */
33
+ function arrowToColumns(data) {
34
+ const { numRows, numCols, schema: { fields } } = data;
35
+ const columns = {};
36
+
37
+ for (let col = 0; col < numCols; ++col) {
38
+ const name = fields[col].name;
39
+ if (columns[name]) {
40
+ console.warn(`Redundant column name "${name}". Skipping...`);
41
+ } else {
42
+ columns[name] = convertArrowColumn(data.getChildAt(col));
43
+ }
44
+ }
45
+
46
+ return { numRows, columns };
47
+ }
48
+
49
+ /**
50
+ * Convert an array of values to a set of column arrays.
51
+ * If the array values are objects, build out named columns.
52
+ * We use the keys of the first object as the column names.
53
+ * Otherwise, use a special "values" array.
54
+ * @param {object[]} data An array of data objects.
55
+ * @returns {DataColumns} An object with named column arrays.
56
+ */
57
+ function arrayToColumns(data) {
58
+ const numRows = data.length;
59
+ if (typeof data[0] === 'object') {
60
+ const names = numRows ? Object.keys(data[0]) : [];
61
+ const columns = {};
62
+ if (names.length > 0) {
63
+ names.forEach(name => {
64
+ columns[name] = data.map(d => d[name]);
65
+ });
66
+ }
67
+ return { numRows, columns };
68
+ } else {
69
+ return { numRows, values: data };
70
+ }
71
+ }
@@ -1,81 +0,0 @@
1
- import { Coordinator } from './Coordinator.js';
2
- import { DataCubeIndexer } from './DataCubeIndexer.js';
3
- import { MosaicClient } from './MosaicClient.js';
4
- import { Selection } from './Selection.js';
5
-
6
- export class FilterGroup {
7
- /**
8
- * @param {Coordinator} coordinator The Mosaic coordinator.
9
- * @param {Selection} selection The shared filter selection.
10
- * @param {object|boolean} index Boolean flag or options hash for
11
- * a data cube indexer. Falsy values disable indexing.
12
- */
13
- constructor(coordinator, selection, index = true) {
14
- this.mc = coordinator;
15
- this.selection = selection;
16
- /** @type {Set<MosaicClient>} */
17
- this.clients = new Set();
18
- /** @type {DataCubeIndexer | null} */
19
- this.indexer = null;
20
- this.index(index);
21
-
22
- const { value, activate } = this.handlers = {
23
- value: () => this.update(),
24
- activate: clause => { this.indexer?.index(this.clients, clause); }
25
- };
26
- selection.addEventListener('value', value);
27
- selection.addEventListener('activate', activate);
28
- }
29
-
30
- finalize() {
31
- const { value, activate } = this.handlers;
32
- this.selection.removeEventListener('value', value);
33
- this.selection.removeEventListener('activate', activate);
34
- }
35
-
36
- index(state) {
37
- const { selection } = this;
38
- const { resolver } = selection;
39
- this.indexer = state && (resolver.single || !resolver.union)
40
- ? new DataCubeIndexer(this.mc, { ...state, selection })
41
- : null;
42
- }
43
-
44
- reset() {
45
- this.indexer?.reset();
46
- }
47
-
48
- add(client) {
49
- (this.clients = new Set(this.clients)).add(client);
50
- return this;
51
- }
52
-
53
- remove(client) {
54
- if (this.clients.has(client)) {
55
- (this.clients = new Set(this.clients)).delete(client);
56
- }
57
- return this;
58
- }
59
-
60
- /**
61
- * Internal method to process a selection update.
62
- * The return value is passed as a selection callback value.
63
- * @returns {Promise} A Promise that resolves when the update completes.
64
- */
65
- update() {
66
- const { mc, indexer, clients, selection } = this;
67
- const hasIndex = indexer?.index(clients);
68
- return hasIndex
69
- ? indexer.update()
70
- : defaultUpdate(mc, clients, selection);
71
- }
72
- }
73
-
74
- function defaultUpdate(mc, clients, selection) {
75
- return Promise.all(Array.from(clients).map(client => {
76
- const filter = selection.predicate(client);
77
- if (filter != null) {
78
- return mc.updateClient(client, client.query(filter));
79
- }
80
- }));
81
- }