@uwdata/mosaic-plot 0.12.2 → 0.14.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uwdata/mosaic-plot",
3
- "version": "0.12.2",
3
+ "version": "0.14.0",
4
4
  "description": "A Mosaic-powered plotting framework based on Observable Plot.",
5
5
  "keywords": [
6
6
  "data",
@@ -12,27 +12,24 @@
12
12
  "license": "BSD-3-Clause",
13
13
  "author": "Jeffrey Heer (https://idl.uw.edu)",
14
14
  "type": "module",
15
- "main": "src/index.js",
16
- "module": "src/index.js",
17
- "jsdelivr": "dist/mosaic-plot.min.js",
18
- "unpkg": "dist/mosaic-plot.min.js",
15
+ "exports": {
16
+ "default": "./src/index.js"
17
+ },
19
18
  "repository": {
20
19
  "type": "git",
21
20
  "url": "https://github.com/uwdata/mosaic.git"
22
21
  },
23
22
  "scripts": {
24
- "prebuild": "rimraf dist && mkdir dist",
25
- "build": "node ../../esbuild.js mosaic-plot",
26
23
  "lint": "eslint src test",
27
24
  "test": "vitest run",
28
- "prepublishOnly": "npm run test && npm run lint && npm run build"
25
+ "prepublishOnly": "npm run test && npm run lint"
29
26
  },
30
27
  "dependencies": {
31
- "@observablehq/plot": "^0.6.16",
32
- "@uwdata/mosaic-core": "^0.12.2",
33
- "@uwdata/mosaic-sql": "^0.12.2",
28
+ "@observablehq/plot": "^0.6.17",
29
+ "@uwdata/mosaic-core": "^0.14.0",
30
+ "@uwdata/mosaic-sql": "^0.14.0",
34
31
  "d3": "^7.9.0",
35
32
  "isoformat": "^0.2.1"
36
33
  },
37
- "gitHead": "0ca741d840b98039255f26a5ceedf10be66f790e"
34
+ "gitHead": "a882aab60867e4e9d9738bc950aa9de32729a806"
38
35
  }
@@ -6,6 +6,10 @@ import { getField } from './util/get-field.js';
6
6
  import { invert } from './util/invert.js';
7
7
  import { sanitizeStyles } from './util/sanitize-styles.js';
8
8
 
9
+ /**
10
+ * @import {Activatable} from '@uwdata/mosaic-core'
11
+ * @implements {Activatable}
12
+ */
9
13
  export class Interval1D {
10
14
  constructor(mark, {
11
15
  channel,
@@ -6,6 +6,10 @@ import { getField } from './util/get-field.js';
6
6
  import { invert } from './util/invert.js';
7
7
  import { sanitizeStyles } from './util/sanitize-styles.js';
8
8
 
9
+ /**
10
+ * @import {Activatable} from '@uwdata/mosaic-core'
11
+ * @implements {Activatable}
12
+ */
9
13
  export class Interval2D {
10
14
  constructor(mark, {
11
15
  selection,
@@ -2,6 +2,10 @@ import { clausePoint, clausePoints, isSelection } from '@uwdata/mosaic-core';
2
2
  import { select, pointer, min } from 'd3';
3
3
  import { getField } from './util/get-field.js';
4
4
 
5
+ /**
6
+ * @import {Activatable} from '@uwdata/mosaic-core'
7
+ * @implements {Activatable}
8
+ */
5
9
  export class Nearest {
6
10
  constructor(mark, {
7
11
  selection,
@@ -71,12 +75,14 @@ export class Nearest {
71
75
 
72
76
  // trigger activation updates
73
77
  svg.addEventListener('pointerenter', evt => {
74
- if (!evt.buttons) {
75
- const v = this.channels.map(() => 0);
76
- selection.activate(this.clause(v));
77
- }
78
+ if (!evt.buttons) this.activate();
78
79
  });
79
80
  }
81
+
82
+ activate() {
83
+ const v = this.channels.map(() => 0);
84
+ this.selection.activate(this.clause(v));
85
+ }
80
86
  }
81
87
 
82
88
  /**
@@ -4,6 +4,10 @@ import { getField } from './util/get-field.js';
4
4
 
5
5
  const asc = (a, b) => a - b;
6
6
 
7
+ /**
8
+ * @import {Activatable} from '@uwdata/mosaic-core'
9
+ * @implements {Activatable}
10
+ */
7
11
  export class PanZoom {
8
12
  constructor(mark, {
9
13
  x = new Selection(),
@@ -59,7 +63,7 @@ export class PanZoom {
59
63
  this.svg = svg;
60
64
  if (this.initialized) return; else this.initialized = true;
61
65
 
62
- const { panx, pany, mark: { plot: { element } }, xsel, ysel } = this;
66
+ const { panx, pany, mark: { plot: { element } } } = this;
63
67
 
64
68
  this.xscale = svg.scale('x');
65
69
  this.yscale = svg.scale('y');
@@ -85,19 +89,22 @@ export class PanZoom {
85
89
  let enter = false;
86
90
  element.addEventListener('pointerenter', evt => {
87
91
  if (enter) return; else enter = true;
88
- if (evt.buttons) return; // don't activate if mouse down
89
- if (panx) {
90
- const { xscale, xfield } = this;
91
- xsel.activate(this.clause(xscale.domain, xfield, xscale));
92
- }
93
- if (pany) {
94
- const { yscale, yfield } = this;
95
- ysel.activate(this.clause(yscale.domain, yfield, yscale));
96
- }
92
+ if (!evt.buttons) this.activate(); // don't activate if mouse down
97
93
  });
98
94
  element.addEventListener('pointerleave', () => enter = false);
99
95
  }
100
96
  }
97
+
98
+ activate() {
99
+ if (this.panx) {
100
+ const { xscale, xfield } = this;
101
+ this.xsel.activate(this.clause(xscale.domain, xfield, xscale));
102
+ }
103
+ if (this.pany) {
104
+ const { yscale, yfield } = this;
105
+ this.ysel.activate(this.clause(yscale.domain, yfield, yscale));
106
+ }
107
+ }
101
108
  }
102
109
 
103
110
  function extent(ext, defaultTrue, defaultFalse) {
@@ -8,6 +8,10 @@ import { sanitizeStyles } from './util/sanitize-styles.js';
8
8
  import { neqSome } from './util/neq.js';
9
9
  import { getDatum } from './util/get-datum.js';
10
10
 
11
+ /**
12
+ * @import {Activatable} from '@uwdata/mosaic-core'
13
+ * @implements {Activatable}
14
+ */
11
15
  export class Region {
12
16
  constructor(mark, {
13
17
  channels,
@@ -2,6 +2,10 @@ import { clausePoints } from '@uwdata/mosaic-core';
2
2
  import { getDatum } from './util/get-datum.js';
3
3
  import { neq, neqSome } from './util/neq.js';
4
4
 
5
+ /**
6
+ * @import {Activatable} from '@uwdata/mosaic-core'
7
+ * @implements {Activatable}
8
+ */
5
9
  export class Toggle {
6
10
  /**
7
11
  * @param {*} mark The mark to interact with.
@@ -12,8 +16,8 @@ export class Toggle {
12
16
  channels,
13
17
  peers = true
14
18
  }) {
15
- this.value = null;
16
19
  this.mark = mark;
20
+ this.value = null;
17
21
  this.selection = selection;
18
22
  this.peers = peers;
19
23
  const fields = this.fields = [];
@@ -75,10 +79,13 @@ export class Toggle {
75
79
  });
76
80
 
77
81
  svg.addEventListener('pointerenter', evt => {
78
- if (evt.buttons) return;
79
- this.selection.activate(this.clause([this.fields.map(() => 0)]));
82
+ if (!evt.buttons) this.activate();
80
83
  });
81
84
  }
85
+
86
+ activate() {
87
+ this.selection.activate(this.clause([this.fields.map(() => 0)]));
88
+ }
82
89
  }
83
90
 
84
91
  function isTargetElement(groups, node) {
@@ -39,7 +39,7 @@ export class ConnectedMark extends Mark {
39
39
  .filter(c => c !== as && c !== value);
40
40
  return m4(q, expr, as, value, cols);
41
41
  } else {
42
- return q.orderby(field);
42
+ return q.orderby(as);
43
43
  }
44
44
  }
45
45
  }
package/src/marks/Mark.js CHANGED
@@ -230,7 +230,7 @@ export function markQuery(channels, table, skip = []) {
230
230
  if (skip.includes(channel)) continue;
231
231
 
232
232
  if (channel === 'orderby') {
233
- q.orderby(c.value);
233
+ q.orderby(c.value ?? field);
234
234
  } else if (field) {
235
235
  if (isAggregateExpression(field)) {
236
236
  aggr = true;
package/src/plot.js CHANGED
@@ -10,19 +10,30 @@ const DEFAULT_ATTRIBUTES = {
10
10
  };
11
11
 
12
12
  export class Plot {
13
+ /**
14
+ * @param {HTMLElement} [element]
15
+ */
13
16
  constructor(element) {
17
+ /** @type {Record<string, any>} */
14
18
  this.attributes = { ...DEFAULT_ATTRIBUTES };
15
19
  this.listeners = null;
16
20
  this.interactors = [];
21
+ /** @type {{ legend: import('./legend.js').Legend, include: boolean }[]} */
17
22
  this.legends = [];
23
+ /** @type {import('./marks/Mark.js').Mark[]} */
18
24
  this.marks = [];
25
+ /** @type {Set<import('./marks/Mark.js').Mark> | null} */
19
26
  this.markset = null;
27
+ /** @type {Map<import('@uwdata/mosaic-core').Param, import('./marks/Mark.js').Mark[]>} */
28
+ this.params = new Map;
29
+ /** @type {ReturnType<synchronizer>} */
30
+ this.synch = synchronizer();
31
+
32
+ /** @type {HTMLElement} */
20
33
  this.element = element || document.createElement('div');
21
34
  this.element.setAttribute('class', 'plot');
22
35
  this.element.style.display = 'flex';
23
- this.element.value = this;
24
- this.params = new Map;
25
- this.synch = synchronizer();
36
+ Object.assign(this.element, { value: this });
26
37
  }
27
38
 
28
39
  margins() {
@@ -1,8 +1,6 @@
1
- import { ExprNode, add, dateBin, div, float64, floor, interval, mul, sub } from '@uwdata/mosaic-sql';
1
+ import { ExprNode, binDate, binHistogram } from '@uwdata/mosaic-sql';
2
2
  import { Transform } from '../symbols.js';
3
3
  import { channelScale } from '../marks/util/channel-scale.js';
4
- import { bins } from './bin-step.js';
5
- import { timeInterval } from './time-interval.js';
6
4
 
7
5
  const EXTENT = new Set([
8
6
  'rectY-x', 'rectX-y', 'rect-x', 'rect-y', 'ruleY-x', 'ruleX-y'
@@ -54,29 +52,12 @@ class BinTransformNode extends ExprNode {
54
52
  toString() {
55
53
  const { mark, channel, column, options } = this;
56
54
  const { type, min, max } = mark.channelField(channel);
57
- const { interval: i, steps, offset = 0 } = options;
58
- const ival = i ?? (
59
- type === 'date' || hasTimeScale(mark, channel) ? 'date' : 'number'
60
- );
61
-
62
- let result;
63
- if (ival === 'number') {
64
- // perform number binning
65
- const { apply, sqlApply, sqlInvert } = channelScale(mark, channel);
66
- const b = bins(apply(min), apply(max), options);
67
- const col = sqlApply(column);
68
- const alpha = float64((b.max - b.min) / b.steps);
69
- const bin = floor(div(b.min === 0 ? col : sub(col, b.min), alpha));
70
- const expr = add(b.min, mul(alpha, offset ? add(offset, bin) : bin));
71
- result = sqlInvert(expr);
72
- } else {
73
- // perform date/time binning
74
- const { interval: unit, step = 1 } = ival === 'date'
75
- ? timeInterval(min, max, steps || 40)
76
- : options;
77
- const bin = dateBin(column, unit, step);
78
- result = offset ? add(bin, interval(unit, offset * step)) : bin;
79
- }
55
+ const isDate = options.interval
56
+ || type === 'date'
57
+ || hasTimeScale(mark, channel);
58
+ const result = isDate
59
+ ? binDate(column, [min, max], options)
60
+ : binHistogram(column, [min, max], options, channelScale(mark, channel));
80
61
  return `${result}`;
81
62
  }
82
63
  }
@@ -0,0 +1,3 @@
1
+ import { defineConfig } from 'vite';
2
+
3
+ export default defineConfig({});
@@ -1,43 +0,0 @@
1
- export function binStep(span, steps, minstep = 0, logb = Math.LN10) {
2
- let v;
3
-
4
- const level = Math.ceil(Math.log(steps) / logb);
5
- let step = Math.max(
6
- minstep,
7
- Math.pow(10, Math.round(Math.log(span) / logb) - level)
8
- );
9
-
10
- // increase step size if too many bins
11
- while (Math.ceil(span / step) > steps) { step *= 10; }
12
-
13
- // decrease step size if allowed
14
- const div = [5, 2];
15
- for (let i = 0, n = div.length; i < n; ++i) {
16
- v = step / div[i];
17
- if (v >= minstep && span / v <= steps) step = v;
18
- }
19
-
20
- return step;
21
- }
22
-
23
- export function bins(min, max, options) {
24
- let { step, steps, minstep = 0, nice = true } = options;
25
-
26
- if (nice !== false) {
27
- // use span to determine step size
28
- const span = max - min;
29
- const logb = Math.LN10;
30
- step = step || binStep(span, steps || 25, minstep, logb);
31
-
32
- // adjust min/max relative to step
33
- let v = Math.log(step);
34
- const precision = v >= 0 ? 0 : ~~(-v / logb) + 1;
35
- const eps = Math.pow(10, -precision - 1);
36
- v = Math.floor(min / step + eps) * step;
37
- min = min < v ? v - step : v;
38
- max = Math.ceil(max / step) * step;
39
- steps = Math.round((max - min) / step);
40
- }
41
-
42
- return { min, max, steps };
43
- }
@@ -1,53 +0,0 @@
1
- import { bisector } from 'd3';
2
- import { binStep } from './bin-step.js';
3
-
4
- const YEAR = 'year';
5
- const MONTH = 'month';
6
- const DAY = 'day';
7
- const HOUR = 'hour';
8
- const MINUTE = 'minute';
9
- const SECOND = 'second';
10
- const MILLISECOND = 'millisecond';
11
-
12
- const durationSecond = 1000;
13
- const durationMinute = durationSecond * 60;
14
- const durationHour = durationMinute * 60;
15
- const durationDay = durationHour * 24;
16
- const durationWeek = durationDay * 7;
17
- const durationMonth = durationDay * 30;
18
- const durationYear = durationDay * 365;
19
-
20
- /** @type {[string, number, number][]} */
21
- const intervals = [
22
- [SECOND, 1, durationSecond],
23
- [SECOND, 5, 5 * durationSecond],
24
- [SECOND, 15, 15 * durationSecond],
25
- [SECOND, 30, 30 * durationSecond],
26
- [MINUTE, 1, durationMinute],
27
- [MINUTE, 5, 5 * durationMinute],
28
- [MINUTE, 15, 15 * durationMinute],
29
- [MINUTE, 30, 30 * durationMinute],
30
- [ HOUR, 1, durationHour ],
31
- [ HOUR, 3, 3 * durationHour ],
32
- [ HOUR, 6, 6 * durationHour ],
33
- [ HOUR, 12, 12 * durationHour ],
34
- [ DAY, 1, durationDay ],
35
- [ DAY, 7, durationWeek ],
36
- [ MONTH, 1, durationMonth ],
37
- [ MONTH, 3, 3 * durationMonth ],
38
- [ YEAR, 1, durationYear ]
39
- ];
40
-
41
- export function timeInterval(min, max, steps) {
42
- const span = max - min;
43
- const target = span / steps;
44
- let i = bisector(i => i[2]).right(intervals, target);
45
- if (i === intervals.length) {
46
- return { interval: YEAR, step: binStep(span / durationYear, steps) };
47
- } else if (i) {
48
- i = intervals[target / intervals[i - 1][2] < intervals[i][2] / target ? i - 1 : i];
49
- return { interval: i[0], step: i[1] };
50
- } else {
51
- return { interval: MILLISECOND, step: binStep(span, steps, 1) };
52
- }
53
- }