@uwdata/mosaic-plot 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uwdata/mosaic-plot",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "A Mosaic-powered plotting framework based on Observable Plot.",
5
5
  "keywords": [
6
6
  "data",
@@ -10,7 +10,7 @@
10
10
  "mosaic"
11
11
  ],
12
12
  "license": "BSD-3-Clause",
13
- "author": "Jeffrey Heer (http://idl.cs.washington.edu)",
13
+ "author": "Jeffrey Heer (https://idl.uw.edu)",
14
14
  "type": "module",
15
15
  "main": "src/index.js",
16
16
  "module": "src/index.js",
@@ -28,11 +28,11 @@
28
28
  "prepublishOnly": "npm run test && npm run lint && npm run build"
29
29
  },
30
30
  "dependencies": {
31
- "@observablehq/plot": "^0.6.14",
32
- "@uwdata/mosaic-core": "^0.9.0",
33
- "@uwdata/mosaic-sql": "^0.9.0",
31
+ "@observablehq/plot": "^0.6.15",
32
+ "@uwdata/mosaic-core": "^0.10.0",
33
+ "@uwdata/mosaic-sql": "^0.10.0",
34
34
  "d3": "^7.9.0",
35
35
  "isoformat": "^0.2.1"
36
36
  },
37
- "gitHead": "89bb9b0dfa747aed691eaeba35379525a6764c61"
37
+ "gitHead": "94fc4f0d4efc622001f6afd6714d1e9dda745be2"
38
38
  }
@@ -1,4 +1,4 @@
1
- import { interval } from '@uwdata/mosaic-core';
1
+ import { clauseInterval } from '@uwdata/mosaic-core';
2
2
  import { ascending, min, max, select } from 'd3';
3
3
  import { brushX, brushY } from './util/brush.js';
4
4
  import { closeTo } from './util/close-to.js';
@@ -52,7 +52,7 @@ export class Interval1D {
52
52
 
53
53
  clause(value) {
54
54
  const { mark, pixelSize, field, scale } = this;
55
- return interval(field, value, {
55
+ return clauseInterval(field, value, {
56
56
  source: this,
57
57
  clients: this.peers ? mark.plot.markSet : new Set().add(mark),
58
58
  scale,
@@ -1,4 +1,4 @@
1
- import { intervals } from '@uwdata/mosaic-core';
1
+ import { clauseIntervals } from '@uwdata/mosaic-core';
2
2
  import { ascending, min, max, select } from 'd3';
3
3
  import { brush } from './util/brush.js';
4
4
  import { closeTo } from './util/close-to.js';
@@ -55,7 +55,7 @@ export class Interval2D {
55
55
 
56
56
  clause(value) {
57
57
  const { mark, pixelSize, xfield, yfield, xscale, yscale } = this;
58
- return intervals([xfield, yfield], value, {
58
+ return clauseIntervals([xfield, yfield], value, {
59
59
  source: this,
60
60
  clients: this.peers ? mark.plot.markSet : new Set().add(mark),
61
61
  scales: [xscale, yscale],
@@ -1,4 +1,4 @@
1
- import { isSelection, points } from '@uwdata/mosaic-core';
1
+ import { clausePoints, isSelection } from '@uwdata/mosaic-core';
2
2
  import { select, pointer } from 'd3';
3
3
  import { getField } from './util/get-field.js';
4
4
 
@@ -24,7 +24,10 @@ export class Nearest {
24
24
 
25
25
  clause(value) {
26
26
  const { clients, fields } = this;
27
- return points(fields, value ? [value] : value, { source: this, clients });
27
+ return clausePoints(fields, value ? [value] : value, {
28
+ source: this,
29
+ clients
30
+ });
28
31
  }
29
32
 
30
33
  init(svg) {
@@ -1,4 +1,4 @@
1
- import { interval, Selection } from '@uwdata/mosaic-core';
1
+ import { Selection, clauseInterval } from '@uwdata/mosaic-core';
2
2
  import { select, zoom, ZoomTransform } from 'd3';
3
3
  import { getField } from './util/get-field.js';
4
4
 
@@ -48,7 +48,7 @@ export class PanZoom {
48
48
  }
49
49
 
50
50
  clause(value, field, scale) {
51
- return interval(field, value, {
51
+ return clauseInterval(field, value, {
52
52
  source: this,
53
53
  clients: this.mark.plot.markSet,
54
54
  scale
@@ -1,4 +1,4 @@
1
- import { points } from '@uwdata/mosaic-core';
1
+ import { clausePoints } from '@uwdata/mosaic-core';
2
2
 
3
3
  export class Toggle {
4
4
  /**
@@ -35,7 +35,7 @@ export class Toggle {
35
35
 
36
36
  clause(value) {
37
37
  const { fields, mark } = this;
38
- return points(fields, value, {
38
+ return clausePoints(fields, value, {
39
39
  source: this,
40
40
  clients: this.peers ? mark.plot.markSet : new Set().add(mark)
41
41
  });
package/src/legend.js CHANGED
@@ -10,7 +10,7 @@ export class Legend {
10
10
  constructor(channel, options) {
11
11
  const { as, field, ...rest } = options;
12
12
  this.channel = channel;
13
- this.options = { label: null, ...rest };
13
+ this.options = rest;
14
14
  this.type = null;
15
15
  this.handler = null;
16
16
  this.selection = as;
@@ -51,10 +51,15 @@ export class Legend {
51
51
  }
52
52
 
53
53
  function createLegend(legend, svg) {
54
- const { channel, options, selection } = legend;
54
+ const { channel, plot, selection } = legend;
55
55
  const scale = svg.scale(channel);
56
56
  const type = scale.type === 'ordinal' ? SWATCH : RAMP;
57
57
 
58
+ const options = {
59
+ label: plot.getAttribute(`${channel}Label`) ?? null,
60
+ ...legend.options
61
+ };
62
+
58
63
  // labels for swatch legends are not yet supported by Plot
59
64
  // track here: https://github.com/observablehq/plot/issues/834
60
65
  // for consistent layout, adjust sizing when there is no label
@@ -1,3 +1,4 @@
1
+ import { toDataColumns } from '@uwdata/mosaic-core';
1
2
  import { Query, gt, isBetween, sql, sum } from '@uwdata/mosaic-sql';
2
3
  import { Transient } from '../symbols.js';
3
4
  import { binExpr } from './util/bin-expr.js';
@@ -6,7 +7,6 @@ import { extentX, extentY, xext, yext } from './util/extent.js';
6
7
  import { grid1d } from './util/grid.js';
7
8
  import { handleParam } from './util/handle-param.js';
8
9
  import { Mark, channelOption, markQuery } from './Mark.js';
9
- import { toDataColumns } from './util/to-data-columns.js';
10
10
 
11
11
  export class Density1DMark extends Mark {
12
12
  constructor(type, source, options) {
@@ -1,8 +1,8 @@
1
+ import { toDataColumns } from '@uwdata/mosaic-core';
1
2
  import { avg, count, stddev } from '@uwdata/mosaic-sql';
2
3
  import { erfinv } from './util/stats.js';
3
4
  import { Mark, markPlotSpec, markQuery } from './Mark.js';
4
5
  import { handleParam } from './util/handle-param.js';
5
- import { toDataColumns } from './util/to-data-columns.js';
6
6
 
7
7
  export class ErrorBarMark extends Mark {
8
8
  constructor(type, source, options) {
@@ -1,4 +1,5 @@
1
1
  import { interpolatorBarycentric, interpolateNearest, interpolatorRandomWalk } from '@observablehq/plot';
2
+ import { toDataColumns } from '@uwdata/mosaic-core';
2
3
  import { Query, count, isBetween, lt, lte, neq, sql, sum } from '@uwdata/mosaic-sql';
3
4
  import { Transient } from '../symbols.js';
4
5
  import { binExpr } from './util/bin-expr.js';
@@ -6,7 +7,6 @@ import { dericheConfig, dericheConv2d } from './util/density.js';
6
7
  import { extentX, extentY, xyext } from './util/extent.js';
7
8
  import { grid2d } from './util/grid.js';
8
9
  import { handleParam } from './util/handle-param.js';
9
- import { toDataColumns } from './util/to-data-columns.js';
10
10
  import { Mark } from './Mark.js';
11
11
 
12
12
  export const DENSITY = 'density';
@@ -25,72 +25,62 @@ export class HexbinMark extends Mark {
25
25
  if (this.hasOwnData()) return null;
26
26
  const { plot, binWidth, channels, source } = this;
27
27
 
28
- // get x / y extents, may update plot domainX / domainY
29
- const [x1, x2] = extentX(this, filter);
30
- const [y1, y2] = extentY(this, filter);
31
-
32
- // Adjust screen-space coordinates by top/left
33
- // margins as this is what Observable Plot does.
34
- // TODO use zero margins when faceted?
35
- const ox = 0.5 - plot.getAttribute('marginLeft');
36
- const oy = 0 - plot.getAttribute('marginTop');
37
- const dx = `${binWidth}::DOUBLE`;
38
- const dy = `${binWidth * (1.5 / Math.sqrt(3))}::DOUBLE`;
39
- const xr = `${plot.innerWidth() / (x2 - x1)}::DOUBLE`;
40
- const yr = `${plot.innerHeight() / (y2 - y1)}::DOUBLE`;
41
-
42
28
  // Extract channel information, update top-level query
43
29
  // and extract dependent columns for aggregates
44
30
  let x, y;
45
- const aggr = new Set;
31
+ const dims = new Set;
46
32
  const cols = {};
47
- let orderby;
48
33
  for (const c of channels) {
49
34
  if (c.channel === 'orderby') {
50
- orderby = c.value; // TODO revisit once groupby is added
35
+ // ignore ordering, as we will aggregate
51
36
  } else if (c.channel === 'x') {
52
37
  x = c;
53
38
  } else if (c.channel === 'y') {
54
39
  y = c;
55
40
  } else if (Object.hasOwn(c, 'field')) {
56
- cols[c.as] = c.field;
57
- if (c.field.aggregate) {
58
- c.field.columns.forEach(col => aggr.add(col));
41
+ const { as, field } = c;
42
+ cols[as] = field;
43
+ if (!field.aggregate) {
44
+ dims.add(as);
59
45
  }
60
46
  }
61
47
  }
62
48
 
63
- // Top-level query; we add a hex binning subquery below
64
- // Maps binned screen space coordinates back to data
65
- // values to ensure we get correct data-driven scales
66
- const q = Query.select({
67
- [x.as]: sql`${x1}::DOUBLE + ((x + 0.5 * (y & 1)) * ${dx} + ${ox})::DOUBLE / ${xr}`,
68
- [y.as]: sql`${y2}::DOUBLE - (y * ${dy} + ${oy})::DOUBLE / ${yr}`,
69
- ...cols
70
- }).groupby('x', 'y');
71
-
72
- if (orderby) q.orderby(orderby);
49
+ // get x / y extents, may update plot xDomain / yDomain
50
+ const [x1, x2] = extentX(this, filter);
51
+ const [y1, y2] = extentY(this, filter);
73
52
 
74
- // Map x/y channels to screen space
75
- const xx = `${xr} * (${x.field} - ${x1}::DOUBLE)`;
76
- const yy = `${yr} * (${y2}::DOUBLE - ${y.field})`;
53
+ // Adjust screen-space coordinates by top/left
54
+ // margins as this is what Observable Plot does.
55
+ const ox = 0.5 - plot.getAttribute('marginLeft');
56
+ const oy = 0 - plot.getAttribute('marginTop');
57
+ const dx = `${binWidth}::DOUBLE`;
58
+ const dy = `${binWidth * (1.5 / Math.sqrt(3))}::DOUBLE`;
59
+ const xr = `${plot.innerWidth() / (x2 - x1)}::DOUBLE`;
60
+ const yr = `${plot.innerHeight() / (y2 - y1)}::DOUBLE`;
77
61
 
78
- // Perform hex binning of x/y coordinates
79
- // TODO add groupby dims
80
- const hex = Query
81
- .select({
82
- py: sql`(${yy} - ${oy}) / ${dy}`,
83
- pj: sql`ROUND(py)::INTEGER`,
84
- px: sql`(${xx} - ${ox}) / ${dx} - 0.5 * (pj & 1)`,
85
- pi: sql`ROUND(px)::INTEGER`,
86
- tt: sql`ABS(py-pj) * 3 > 1 AND (px-pi)**2 + (py-pj)**2 > (px - pi - 0.5 * CASE WHEN px < pi THEN -1 ELSE 1 END)**2 + (py - pj - CASE WHEN py < pj THEN -1 ELSE 1 END)**2`,
87
- x: sql`CASE WHEN tt THEN (pi + (CASE WHEN px < pi THEN -0.5 ELSE 0.5 END) + (CASE WHEN pj & 1 <> 0 THEN 0.5 ELSE -0.5 END))::INTEGER ELSE pi END`,
88
- y: sql`CASE WHEN tt THEN (pj + CASE WHEN py < pj THEN -1 ELSE 1 END)::INTEGER ELSE pj END`
62
+ // Top-level query maps from screen space back to data values.
63
+ // Doing so ensures that Plot generates correct data-driven scales.
64
+ return Query.select({
65
+ [x.as]: sql`${x1}::DOUBLE + ((_x + 0.5 * (_y & 1)) * ${dx} + ${ox})::DOUBLE / ${xr}`,
66
+ [y.as]: sql`${y2}::DOUBLE - (_y * ${dy} + ${oy})::DOUBLE / ${yr}`,
67
+ ...cols
89
68
  })
90
- .select(Array.from(aggr))
91
- .from(source.table)
92
- .where(isNotNull(x.field), isNotNull(y.field), filter)
93
-
94
- return q.from(hex);
69
+ .groupby('_x', '_y', ...dims)
70
+ .from(
71
+ // Subquery performs hex binning in screen space and also passes
72
+ // original columns through (the DB should optimize this).
73
+ Query.select({
74
+ _py: sql`(${yr} * (${y2}::DOUBLE - ${y.field}) - ${oy}) / ${dy}`,
75
+ _pj: sql`ROUND(_py)::INTEGER`,
76
+ _px: sql`(${xr} * (${x.field} - ${x1}::DOUBLE) - ${ox}) / ${dx} - 0.5 * (_pj & 1)`,
77
+ _pi: sql`ROUND(_px)::INTEGER`,
78
+ _tt: sql`ABS(_py-_pj) * 3 > 1 AND (_px-_pi)**2 + (_py-_pj)**2 > (_px - _pi - 0.5 * CASE WHEN _px < _pi THEN -1 ELSE 1 END)**2 + (_py - _pj - CASE WHEN _py < _pj THEN -1 ELSE 1 END)**2`,
79
+ _x: sql`CASE WHEN _tt THEN (_pi + (CASE WHEN _px < _pi THEN -0.5 ELSE 0.5 END) + (CASE WHEN _pj & 1 <> 0 THEN 0.5 ELSE -0.5 END))::INTEGER ELSE _pi END`,
80
+ _y: sql`CASE WHEN _tt THEN (_pj + CASE WHEN _py < _pj THEN -1 ELSE 1 END)::INTEGER ELSE _pj END`
81
+ }, '*')
82
+ .from(source.table)
83
+ .where(isNotNull(x.field), isNotNull(y.field), filter)
84
+ );
95
85
  }
96
86
  }
package/src/marks/Mark.js CHANGED
@@ -1,9 +1,8 @@
1
- import { MosaicClient } from '@uwdata/mosaic-core';
1
+ import { MosaicClient, toDataColumns } from '@uwdata/mosaic-core';
2
2
  import { Query, Ref, column, isParamLike } from '@uwdata/mosaic-sql';
3
3
  import { isColor } from './util/is-color.js';
4
4
  import { isConstantOption } from './util/is-constant-option.js';
5
5
  import { isSymbol } from './util/is-symbol.js';
6
- import { toDataColumns } from './util/to-data-columns.js';
7
6
  import { Transform } from '../symbols.js';
8
7
 
9
8
  const isColorChannel = channel => channel === 'stroke' || channel === 'fill';
@@ -1,4 +1,5 @@
1
1
  import { range } from 'd3';
2
+ import { toDataColumns } from '@uwdata/mosaic-core';
2
3
  import {
3
4
  Query, max, min, castDouble, isNotNull,
4
5
  regrIntercept, regrSlope, regrCount,
@@ -7,7 +8,6 @@ import {
7
8
  import { qt } from './util/stats.js';
8
9
  import { Mark, channelOption } from './Mark.js';
9
10
  import { handleParam } from './util/handle-param.js';
10
- import { toDataColumns } from './util/to-data-columns.js';
11
11
 
12
12
  export class RegressionMark extends Mark {
13
13
  constructor(source, options) {
@@ -118,7 +118,7 @@ function concat(a, b) {
118
118
 
119
119
  function linePoints(fit) {
120
120
  // eslint-disable-next-line no-unused-vars
121
- const { x0, x1, xm, intercept, slope, n, ssx, ssy, ...rest } = fit.columns;
121
+ const { x0 = [], x1 = [], xm, intercept, slope, n, ssx, ssy, ...rest } = fit.columns;
122
122
  const predict = (x, i) => intercept[i] + x * slope[i];
123
123
  const x = concat(x0, x1);
124
124
  const y = concat(x0.map(predict), x1.map(predict));
@@ -182,6 +182,7 @@ export const attributeMap = new Map([
182
182
  ['rRange', 'r.range'],
183
183
  ['rClamp', 'r.clamp'],
184
184
  ['rNice', 'r.nice'],
185
+ ['rLabel', 'r.label'],
185
186
  ['rPercent', 'r.percent'],
186
187
  ['rZero', 'r.zero'],
187
188
  ['rBase', 'r.base'],
package/src/plot.js CHANGED
@@ -42,8 +42,9 @@ export class Plot {
42
42
  innerHeight(defaultValue = 400) {
43
43
  const { top, bottom } = this.margins();
44
44
  let h = this.getAttribute('height');
45
- if (h == null && defaultValue != null) {
46
- h = defaultValue; // TODO could apply more nuanced logic here
45
+ if (h == null) {
46
+ // TODO could apply more nuanced logic here?
47
+ h = maybeAspectRatio(this, top, bottom) || defaultValue;
47
48
  this.setAttribute('height', h, { silent: true });
48
49
  }
49
50
  return h - top - bottom;
@@ -159,3 +160,14 @@ export class Plot {
159
160
  this.legends.push({ legend, include });
160
161
  }
161
162
  }
163
+
164
+ function maybeAspectRatio(plot, top, bottom) {
165
+ const ar = plot.getAttribute('aspectRatio');
166
+ if (ar == null) return;
167
+ const x = plot.getAttribute('xDomain');
168
+ const y = plot.getAttribute('yDomain');
169
+ if (!x || !y) return;
170
+ const dx = Math.abs(x[1] - x[0]);
171
+ const dy = Math.abs(y[1] - y[0]);
172
+ return dy * plot.innerWidth() / (ar * dx) + top + bottom;
173
+ }
@@ -0,0 +1,43 @@
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,13 +1,20 @@
1
+ import { dateBin } from '@uwdata/mosaic-sql';
1
2
  import { Transform } from '../symbols.js';
2
3
  import { channelScale } from '../marks/util/channel-scale.js';
4
+ import { bins } from './bin-step.js';
5
+ import { timeInterval } from './time-interval.js';
3
6
 
4
7
  const EXTENT = new Set([
5
8
  'rectY-x', 'rectX-y', 'rect-x', 'rect-y', 'ruleY-x', 'ruleX-y'
6
9
  ]);
7
10
 
8
- export function bin(field, options = { steps: 25 }) {
11
+ export function hasExtent(mark, channel) {
12
+ return EXTENT.has(`${mark.type}-${channel}`);
13
+ }
14
+
15
+ export function bin(field, options = {}) {
9
16
  const fn = (mark, channel) => {
10
- if (EXTENT.has(`${mark.type}-${channel}`)) {
17
+ if (hasExtent(mark, channel)) {
11
18
  return {
12
19
  [`${channel}1`]: binField(mark, channel, field, options),
13
20
  [`${channel}2`]: binField(mark, channel, field, { ...options, offset: 1 })
@@ -26,56 +33,39 @@ function binField(mark, channel, column, options) {
26
33
  return {
27
34
  column,
28
35
  label: column,
29
- get stats() { return { column, stats: ['min', 'max'] }; },
30
36
  get columns() { return [column]; },
31
37
  get basis() { return column; },
38
+ get stats() { return { column, stats: ['min', 'max'] }; },
32
39
  toString() {
33
- const { apply, sqlApply, sqlInvert } = channelScale(mark, channel);
34
- const { min, max } = mark.channelField(channel);
35
- const b = bins(apply(min), apply(max), options);
36
- const col = sqlApply(column);
37
- const base = b.min === 0 ? col : `(${col} - ${b.min})`;
38
- const alpha = `${(b.max - b.min) / b.steps}::DOUBLE`;
39
- const off = options.offset ? `${options.offset} + ` : '';
40
- const expr = `${b.min} + ${alpha} * (${off}FLOOR(${base} / ${alpha}))`;
41
- return `${sqlInvert(expr)}`;
40
+ const { type, min, max } = mark.channelField(channel);
41
+ const { interval: i, steps, offset = 0 } = options;
42
+ const interval = i ?? (
43
+ type === 'date' || hasTimeScale(mark, channel) ? 'date' : 'number'
44
+ );
45
+
46
+ if (interval === 'number') {
47
+ // perform number binning
48
+ const { apply, sqlApply, sqlInvert } = channelScale(mark, channel);
49
+ const b = bins(apply(min), apply(max), options);
50
+ const col = sqlApply(column);
51
+ const base = b.min === 0 ? col : `(${col} - ${b.min})`;
52
+ const alpha = `${(b.max - b.min) / b.steps}::DOUBLE`;
53
+ const off = offset ? `${offset} + ` : '';
54
+ const expr = `${b.min} + ${alpha} * (${off}FLOOR(${base} / ${alpha}))`;
55
+ return `${sqlInvert(expr)}`;
56
+ } else {
57
+ // perform date/time binning
58
+ const { interval: unit, step = 1 } = interval === 'date'
59
+ ? timeInterval(min, max, steps || 40)
60
+ : options;
61
+ const off = offset ? ` + INTERVAL ${offset * step} ${unit}` : '';
62
+ return `(${dateBin(column, unit, step)}${off})`;
63
+ }
42
64
  }
43
65
  };
44
66
  }
45
67
 
46
- export function bins(min, max, options) {
47
- let { steps = 25, minstep = 0, nice = true } = options;
48
-
49
- if (nice !== false) {
50
- // use span to determine step size
51
- const span = max - min;
52
- const maxb = steps;
53
- const logb = Math.LN10;
54
- const level = Math.ceil(Math.log(maxb) / logb);
55
- let step = Math.max(
56
- minstep,
57
- Math.pow(10, Math.round(Math.log(span) / logb) - level)
58
- );
59
-
60
- // increase step size if too many bins
61
- while (Math.ceil(span / step) > maxb) { step *= 10; }
62
-
63
- // decrease step size if allowed
64
- const div = [5, 2];
65
- let v;
66
- for (let i = 0, n = div.length; i < n; ++i) {
67
- v = step / div[i];
68
- if (v >= minstep && span / v <= maxb) step = v;
69
- }
70
-
71
- v = Math.log(step);
72
- const precision = v >= 0 ? 0 : ~~(-v / logb) + 1;
73
- const eps = Math.pow(10, -precision - 1);
74
- v = Math.floor(min / step + eps) * step;
75
- min = min < v ? v - step : v;
76
- max = Math.ceil(max / step) * step;
77
- steps = Math.round((max - min) / step);
78
- }
79
-
80
- return { min, max, steps };
68
+ function hasTimeScale(mark, channel) {
69
+ const scale = mark.plot.getAttribute(`${channel}Scale`);
70
+ return scale === 'utc' || scale === 'time';
81
71
  }
@@ -0,0 +1,53 @@
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, 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
+ }
@@ -1,71 +0,0 @@
1
- import { convertArrowColumn, isArrowTable } from '@uwdata/mosaic-core';
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
- }