@uwdata/mosaic-plot 0.7.1 → 0.8.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.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "A Mosaic-powered plotting framework based on Observable Plot.",
5
5
  "keywords": [
6
6
  "data",
@@ -23,16 +23,16 @@
23
23
  "scripts": {
24
24
  "prebuild": "rimraf dist && mkdir dist",
25
25
  "build": "node ../../esbuild.js mosaic-plot",
26
- "lint": "eslint src test --ext .js",
26
+ "lint": "eslint src test",
27
27
  "test": "mocha 'test/**/*-test.js'",
28
28
  "prepublishOnly": "npm run test && npm run lint && npm run build"
29
29
  },
30
30
  "dependencies": {
31
31
  "@observablehq/plot": "^0.6.14",
32
- "@uwdata/mosaic-core": "^0.7.1",
33
- "@uwdata/mosaic-sql": "^0.7.0",
32
+ "@uwdata/mosaic-core": "^0.8.0",
33
+ "@uwdata/mosaic-sql": "^0.8.0",
34
34
  "d3": "^7.9.0",
35
35
  "isoformat": "^0.2.1"
36
36
  },
37
- "gitHead": "7e6f3ea9b3011ea2c9201c1aa16e8e5664621a4c"
37
+ "gitHead": "a24b4c9f7dfa1c38c6af96ec17e075326c1af9b0"
38
38
  }
@@ -11,7 +11,7 @@ export class Interval1D {
11
11
  constructor(mark, {
12
12
  channel,
13
13
  selection,
14
- field,
14
+ field = undefined,
15
15
  pixelSize = 1,
16
16
  peers = true,
17
17
  brush: style
@@ -61,7 +61,7 @@ export class Interval1D {
61
61
  };
62
62
  }
63
63
 
64
- init(svg) {
64
+ init(svg, root) {
65
65
  const { brush, channel, style } = this;
66
66
  this.scale = svg.scale(channel);
67
67
 
@@ -70,7 +70,7 @@ export class Interval1D {
70
70
  brush.extent([[min(rx), min(ry)], [max(rx), max(ry)]]);
71
71
 
72
72
  const facets = select(svg).selectAll('g[aria-label="facet"]');
73
- const root = facets.size() ? facets : select(svg);
73
+ root = facets.size() ? facets : select(root ?? svg);
74
74
  this.g = root
75
75
  .append('g')
76
76
  .attr('class', `interval-${channel}`)
@@ -85,6 +85,8 @@ export class Interval1D {
85
85
  }
86
86
  }
87
87
 
88
- svg.addEventListener('pointerenter', () => this.activate());
88
+ svg.addEventListener('pointerenter', evt => {
89
+ if (!evt.buttons) this.activate();
90
+ });
89
91
  }
90
92
  }
@@ -97,6 +97,8 @@ export class Interval2D {
97
97
  this.g.call(brush.moveSilent, [[x1, y1], [x2, y2]]);
98
98
  }
99
99
 
100
- svg.addEventListener('pointerenter', () => this.activate());
100
+ svg.addEventListener('pointerenter', evt => {
101
+ if (!evt.buttons) this.activate();
102
+ });
101
103
  }
102
104
  }
@@ -41,26 +41,27 @@ export class Nearest {
41
41
 
42
42
  root.on('pointerdown pointermove', function(evt) {
43
43
  const [x, y] = pointer(evt, this);
44
- const z = findNearest(data, key, scale.invert(channel === 'x' ? x : y));
44
+ const z = findNearest(data.columns[key], scale.invert(channel === 'x' ? x : y));
45
45
  selection.update(param ? z : that.clause(z));
46
46
  });
47
47
 
48
48
  if (param) return;
49
- svg.addEventListener('pointerenter', () => {
50
- this.selection.activate(this.clause(0));
49
+ svg.addEventListener('pointerenter', evt => {
50
+ if (!evt.buttons) this.selection.activate(this.clause(0));
51
51
  });
52
52
  }
53
53
  }
54
54
 
55
- function findNearest(data, key, value) {
55
+ function findNearest(values, value) {
56
56
  let dist = Infinity;
57
- let v;
58
- data.forEach(d => {
59
- const delta = Math.abs(d[key] - value);
57
+ let nearest;
58
+
59
+ for (let i = 0; i < values.length; ++i) {
60
+ const delta = Math.abs(values[i] - value);
60
61
  if (delta < dist) {
61
62
  dist = delta;
62
- v = d[key];
63
+ nearest = values[i];
63
64
  }
64
- });
65
- return v;
65
+ }
66
+ return nearest;
66
67
  }
@@ -86,8 +86,9 @@ export class PanZoom {
86
86
 
87
87
  if (panx || pany) {
88
88
  let enter = false;
89
- element.addEventListener('mouseenter', () => {
89
+ element.addEventListener('pointerenter', evt => {
90
90
  if (enter) return; else enter = true;
91
+ if (evt.buttons) return; // don't activate if mouse down
91
92
  if (panx) {
92
93
  const { xscale, xfield } = this;
93
94
  xsel.activate(this.clause(xscale.domain, xfield, xscale));
@@ -97,7 +98,7 @@ export class PanZoom {
97
98
  ysel.activate(this.clause(yscale.domain, yfield, yscale));
98
99
  }
99
100
  });
100
- element.addEventListener('mouseleave', () => enter = false);
101
+ element.addEventListener('pointerleave', () => enter = false);
101
102
  }
102
103
  }
103
104
  }
@@ -1,6 +1,10 @@
1
1
  import { and, or, isNotDistinct, literal } from '@uwdata/mosaic-sql';
2
2
 
3
3
  export class Toggle {
4
+ /**
5
+ * @param {*} mark The mark to interact with.
6
+ * @param {*} options The interactor options.
7
+ */
4
8
  constructor(mark, {
5
9
  selection,
6
10
  channels,
@@ -11,12 +15,12 @@ export class Toggle {
11
15
  this.selection = selection;
12
16
  this.peers = peers;
13
17
  this.channels = channels.map(c => {
14
- const q = c === 'color' ? ['fill', 'stroke']
18
+ const q = c === 'color' ? ['color', 'fill', 'stroke']
15
19
  : c === 'x' ? ['x', 'x1', 'x2']
16
20
  : c === 'y' ? ['y', 'y1', 'y2']
17
21
  : [c];
18
22
  for (let i = 0; i < q.length; ++i) {
19
- const f = mark.channelField(q[i]);
23
+ const f = mark.channelField(q[i], { exact: true });
20
24
  if (f) return {
21
25
  field: f.field?.basis || f.field,
22
26
  as: f.as
@@ -51,13 +55,9 @@ export class Toggle {
51
55
 
52
56
  init(svg, selector, accessor) {
53
57
  const { mark, channels, selection } = this;
54
- const { data } = mark;
55
- accessor = accessor || (target => {
56
- const datum = data[target.__data__];
57
- return channels.map(c => datum[c.as]);
58
- });
59
-
60
- selector = selector || `[data-index="${mark.index}"]`;
58
+ const { data: { columns = {} } = {} } = mark;
59
+ accessor ??= target => channels.map(c => columns[c.as][target.__data__]);
60
+ selector ??= `[data-index="${mark.index}"]`;
61
61
  const groups = new Set(svg.querySelectorAll(selector));
62
62
 
63
63
  svg.addEventListener('pointerdown', evt => {
@@ -67,7 +67,7 @@ export class Toggle {
67
67
 
68
68
  if (isTargetElement(groups, target)) {
69
69
  const point = accessor(target);
70
- if (evt.shiftKey && state?.length) {
70
+ if ((evt.shiftKey || evt.metaKey) && state?.length) {
71
71
  value = state.filter(s => neq(s, point));
72
72
  if (value.length === state.length) value.push(point);
73
73
  } else if (state?.length === 1 && !neq(state[0], point)) {
@@ -83,7 +83,8 @@ export class Toggle {
83
83
  }
84
84
  });
85
85
 
86
- svg.addEventListener('pointerenter', () => {
86
+ svg.addEventListener('pointerenter', evt => {
87
+ if (evt.buttons) return;
87
88
  this.selection.activate(this.clause([this.channels.map(() => 0)]));
88
89
  });
89
90
  }
@@ -4,6 +4,8 @@
4
4
  * even after the node is removed from the DOM.
5
5
  */
6
6
  export function patchScreenCTM() {
7
+ /** @type {SVGGraphicsElement} */
8
+ // @ts-ignore
7
9
  const node = this;
8
10
  const getScreenCTM = node.getScreenCTM;
9
11
  let memo;
package/src/legend.js CHANGED
@@ -1,40 +1,35 @@
1
+ import { scale } from '@observablehq/plot';
2
+ import { Interval1D } from './interactors/Interval1D.js';
1
3
  import { Toggle } from './interactors/Toggle.js';
2
4
 
5
+ const TOGGLE_SELECTOR = ':scope > div, :scope > span';
6
+ const SWATCH = 'swatch';
7
+ const RAMP = 'ramp';
8
+
3
9
  export class Legend {
4
10
  constructor(channel, options) {
5
- const { as, ...rest } = options;
11
+ const { as, field, ...rest } = options;
6
12
  this.channel = channel;
7
13
  this.options = { label: null, ...rest };
14
+ this.type = null;
15
+ this.handler = null;
8
16
  this.selection = as;
17
+ this.field = field;
18
+ this.legend = null;
9
19
 
10
20
  this.element = document.createElement('div');
11
21
  this.element.setAttribute('class', 'legend');
12
- this.element.value = this;
22
+ Object.assign(this.element, { value: this });
13
23
  }
14
24
 
15
25
  setPlot(plot) {
16
- const { channel, selection } = this;
17
- const mark = findMark(plot, channel);
18
- if (this.selection && mark) {
19
- this.handler = new Toggle(mark, { selection, channels: [channel] });
20
- this.selection.addEventListener('value', () => this.update());
21
- }
26
+ this.plot = plot;
22
27
  }
23
28
 
24
29
  init(svg) {
25
- const { channel, options, handler } = this;
26
- const scale = svg.scale(channel);
27
- const opt = scale.type === 'ordinal'
28
- ? options
29
- : { marginTop: 1, tickSize: 2, height: 28, ...options };
30
- this.legend = svg.legend(channel, opt);
31
-
32
- if (handler) {
33
- handler.init(this.legend, ':scope > div', el => [el.__data__]);
34
- this.update();
35
- }
36
-
37
- this.element.replaceChildren(this.legend);
30
+ // createLegend sets this.legend, may set this.handler
31
+ const el = createLegend(this, svg);
32
+ this.element.replaceChildren(el);
38
33
  return this.element;
39
34
  }
40
35
 
@@ -42,7 +37,7 @@ export class Legend {
42
37
  if (!this.legend) return;
43
38
  const { value } = this.selection;
44
39
  const curr = value && value.length ? new Set(value.map(v => v[0])) : null;
45
- const nodes = this.legend.querySelectorAll(':scope > div');
40
+ const nodes = this.legend.querySelectorAll(TOGGLE_SELECTOR);
46
41
  for (const node of nodes) {
47
42
  const selected = curr ? curr.has(node.__data__) : true;
48
43
  node.style.opacity = selected ? 1 : 0.2;
@@ -50,17 +45,130 @@ export class Legend {
50
45
  }
51
46
  }
52
47
 
53
- function findMark({ marks }, channel) {
48
+ function createLegend(legend, svg) {
49
+ const { channel, options, selection } = legend;
50
+ const scale = svg.scale(channel);
51
+ const type = scale.type === 'ordinal' ? SWATCH : RAMP;
52
+
53
+ // labels for swatch legends are not yet supported by Plot
54
+ // track here: https://github.com/observablehq/plot/issues/834
55
+ // for consistent layout, adjust sizing when there is no label
56
+ const opt = type === SWATCH ? options
57
+ : options.label ? { tickSize: 2, ...options }
58
+ : { tickSize: 2, marginTop: 1, height: 29, ...options };
59
+
60
+ // instantiate new legend element, bind to Legend class
61
+ const el = svg.legend(channel, opt);
62
+ legend.legend = el;
63
+
64
+ // if this is an interactive legend, add a scale lookup function
65
+ // this allows interval interactors to access encoding information
66
+ let interactive = !!selection;
67
+ if (interactive && type === RAMP) {
68
+ const width = opt.width ?? 240; // 240 is default ramp length
69
+ const spatial = spatialScale(scale, width);
70
+ if (spatial) {
71
+ el.scale = function(type) {
72
+ return type === 'x' ? { range: [0, width] }
73
+ : type === 'y' ? { range: [-10, 0] }
74
+ : type === channel ? spatial
75
+ : undefined;
76
+ };
77
+ } else {
78
+ // spatial scale construction failed, disable interaction
79
+ interactive = false;
80
+ }
81
+ }
82
+
83
+ // initialize interactors to use updated legend element
84
+ if (interactive) {
85
+ const handler = getInteractor(legend, type);
86
+ if (type === SWATCH) {
87
+ handler.init(el, TOGGLE_SELECTOR, el => [el.__data__]);
88
+ legend.update();
89
+ } else {
90
+ handler.init(el, el.querySelector('g:last-of-type'));
91
+ }
92
+ }
93
+
94
+ return el;
95
+ }
96
+
97
+ function getInteractor(legend, type) {
98
+ const { channel, handler, selection } = legend;
99
+
100
+ // exit early if already instantiated
101
+ if (handler) return handler;
102
+
103
+ // otherwise instantiate an appropriate interactor
104
+ const mark = interactorMark(legend);
105
+ if (type === SWATCH) {
106
+ legend.handler = new Toggle(mark, { selection, channels: [channel] });
107
+ selection.addEventListener('value', () => legend.update());
108
+ } else {
109
+ const brush = { fill: 'none', stroke: 'currentColor' };
110
+ legend.handler = new Interval1D(mark, { selection, channel, brush });
111
+ }
112
+
113
+ return legend.handler;
114
+ }
115
+
116
+ // generate a faux mark to pass to an interactor
117
+ function interactorMark(legend) {
118
+ const { channel, plot } = legend;
119
+ const field = legend.field ?? findField(plot.marks, channel) ?? 'value';
120
+ if (field) {
121
+ const f = { field };
122
+ return { plot, channelField: c => channel === c ? f : undefined };
123
+ }
124
+ }
125
+
126
+ // search marks for a backing data field for the legend
127
+ function findField(marks, channel) {
54
128
  const channels = channel === 'color' ? ['fill', 'stroke']
55
129
  : channel === 'opacity' ? ['opacity', 'fillOpacity', 'strokeOpacity']
56
130
  : null;
57
131
  if (channels == null) return null;
58
132
  for (let i = marks.length - 1; i > -1; --i) {
59
- for (const channel of channels) {
60
- if (marks[i].channelField(channel, { exact: true })) {
61
- return marks[i];
62
- }
133
+ for (const c of channels) {
134
+ const field = marks[i].channelField(c, { exact: true });
135
+ if (field) return field.field;
63
136
  }
64
137
  }
65
138
  return null;
66
139
  }
140
+
141
+ // generate a spatial scale to brush within color or opacity ramps
142
+ function spatialScale(sourceScale, width) {
143
+ // separate out reusable parts of the scale definition
144
+ // eslint-disable-next-line no-unused-vars
145
+ const { apply, invert, interpolate, ...rest } = sourceScale;
146
+
147
+ // extract basic source scale type
148
+ let src = sourceScale.type;
149
+ if (src.startsWith('diverging-')) src = src.slice(11);
150
+
151
+ // determine spatial scale type
152
+ let type;
153
+ switch (src) {
154
+ case 'log':
155
+ case 'pow':
156
+ case 'sqrt':
157
+ case 'symlog':
158
+ type = src;
159
+ break;
160
+ case 'threshold':
161
+ case 'quantize':
162
+ case 'quantile':
163
+ // these scales do not expose an invert method
164
+ // the legends use color ramps with discrete swatches
165
+ // in the future we could try to support toggle-style
166
+ // interactions that map to threshold range selections
167
+ console.warn(`Legends do not yet support ${src} scales.`);
168
+ return null;
169
+ default:
170
+ type = 'linear';
171
+ }
172
+
173
+ return scale({ x: { ...rest, type, range: [0, width] } });
174
+ }
@@ -11,6 +11,11 @@ export class ConnectedMark extends Mark {
11
11
  this.dim = dim;
12
12
  }
13
13
 
14
+ /**
15
+ * Return a query specifying the data needed by this Mark client.
16
+ * @param {*} [filter] The filtering criteria to apply in the query.
17
+ * @returns {*} The client query
18
+ */
14
19
  query(filter = []) {
15
20
  const { plot, dim, source } = this;
16
21
  const { optimize = true } = source.options || {};
@@ -28,6 +33,7 @@ export class ConnectedMark extends Mark {
28
33
  const [lo, hi] = filteredExtent(filter, field) || [min, max];
29
34
  const [expr] = binExpr(this, dim, size, [lo, hi], 1, as);
30
35
  const cols = q.select()
36
+ // @ts-ignore
31
37
  .map(c => c.as)
32
38
  .filter(c => c !== as && c !== value);
33
39
  return m4(q, expr, as, value, cols);
@@ -13,8 +13,11 @@ export class ContourMark extends Grid2DMark {
13
13
  pixelSize: 2,
14
14
  ...channels
15
15
  });
16
- handleParam(this, 'thresholds', thresholds, () => {
17
- return this.grids ? this.contours().update() : null
16
+
17
+ /** @type {number|number[]} */
18
+ this.thresholds = handleParam(thresholds, value => {
19
+ this.thresholds = value;
20
+ return this.grids ? this.contours().update() : null;
18
21
  });
19
22
  }
20
23
 
@@ -23,12 +26,16 @@ export class ContourMark extends Grid2DMark {
23
26
  }
24
27
 
25
28
  contours() {
26
- const { bins, densityMap, kde, thresholds, plot } = this;
29
+ const { bins, densityMap, grids, thresholds, plot } = this;
30
+ const { numRows, columns } = grids;
27
31
 
28
- let tz = thresholds;
29
- if (!Array.isArray(tz)) {
30
- const [, hi] = gridDomainContinuous(kde, 'density');
31
- tz = Array.from({length: tz - 1}, (_, i) => (hi * (i + 1)) / tz);
32
+ let t = thresholds;
33
+ let tz;
34
+ if (Array.isArray(t)) {
35
+ tz = t;
36
+ } else {
37
+ const [, hi] = gridDomainContinuous(columns.density);
38
+ tz = Array.from({length: t - 1}, (_, i) => (hi * (i + 1)) / t);
32
39
  }
33
40
 
34
41
  if (densityMap.fill || densityMap.stroke) {
@@ -51,18 +58,27 @@ export class ContourMark extends Grid2DMark {
51
58
  const contour = contours().size(bins);
52
59
 
53
60
  // generate contours
54
- this.data = kde.flatMap(cell => tz.map(t => {
55
- return Object.assign(
56
- transform(contour.contour(cell.density, t), x, y),
57
- { ...cell, density: t }
58
- );
59
- }));
61
+ const data = this.contourData = Array(numRows * tz.length);
62
+ const { density, ...groupby } = columns;
63
+ const groups = Object.entries(groupby);
64
+ for (let i = 0, k = 0; i < numRows; ++i) {
65
+ const grid = density[i];
66
+ const rest = groups.reduce((o, [name, col]) => (o[name] = col[i], o), {});
67
+ for (let j = 0; j < tz.length; ++j, ++k) {
68
+ // annotate contour geojson with cell groupby fields
69
+ // d3-contour already adds a threshold "value" property
70
+ data[k] = Object.assign(
71
+ transform(contour.contour(grid, tz[j]), x, y),
72
+ rest
73
+ );
74
+ }
75
+ }
60
76
 
61
77
  return this;
62
78
  }
63
79
 
64
80
  plotSpecs() {
65
- const { type, channels, densityMap, data } = this;
81
+ const { type, channels, densityMap, contourData: data } = this;
66
82
  const options = {};
67
83
  for (const c of channels) {
68
84
  const { channel } = c;
@@ -70,8 +86,12 @@ export class ContourMark extends Grid2DMark {
70
86
  options[channel] = channelOption(c);
71
87
  }
72
88
  }
73
- if (densityMap.fill) options.fill = 'density';
74
- if (densityMap.stroke) options.stroke = 'density';
89
+ // d3-contour adds a threshold "value" property
90
+ // here we ensure requested density values are encoded
91
+ for (const channel in densityMap) {
92
+ if (!densityMap[channel]) continue;
93
+ options[channel] = channelOption({ channel, as: 'value' });
94
+ }
75
95
  return [{ type, data, options }];
76
96
  }
77
97
  }
@@ -8,14 +8,18 @@ export class DenseLineMark extends RasterMark {
8
8
  constructor(source, options) {
9
9
  const { normalize = true, ...rest } = options;
10
10
  super(source, rest);
11
- handleParam(this, 'normalize', normalize);
11
+
12
+ /** @type {boolean} */
13
+ this.normalize = handleParam(normalize, value => {
14
+ return (this.normalize = value, this.requestUpdate());
15
+ });
12
16
  }
13
17
 
14
18
  query(filter = []) {
15
- const { channels, normalize, source, binPad } = this;
16
- const [nx, ny] = this.bins = this.binDimensions(this);
17
- const [x] = binExpr(this, 'x', nx, extentX(this, filter), binPad);
18
- const [y] = binExpr(this, 'y', ny, extentY(this, filter), binPad);
19
+ const { channels, normalize, source, pad } = this;
20
+ const [nx, ny] = this.bins = this.binDimensions();
21
+ const [x] = binExpr(this, 'x', nx, extentX(this, filter), pad);
22
+ const [y] = binExpr(this, 'y', ny, extentY(this, filter), pad);
19
23
 
20
24
  const q = Query
21
25
  .from(source.table)
@@ -6,6 +6,7 @@ import { extentX, extentY, xext, yext } from './util/extent.js';
6
6
  import { grid1d } from './util/grid.js';
7
7
  import { handleParam } from './util/handle-param.js';
8
8
  import { Mark, channelOption, markQuery } from './Mark.js';
9
+ import { toDataColumns } from './util/to-data-columns.js';
9
10
 
10
11
  export class Density1DMark extends Mark {
11
12
  constructor(type, source, options) {
@@ -15,9 +16,15 @@ export class Density1DMark extends Mark {
15
16
  super(type, source, channels, dim === 'x' ? xext : yext);
16
17
  this.dim = dim;
17
18
 
18
- handleParam(this, 'bins', bins);
19
- handleParam(this, 'bandwidth', bandwidth, () => {
20
- return this.grid ? this.convolve().update() : null
19
+ /** @type {number} */
20
+ this.bins = handleParam(bins, value => {
21
+ return (this.bins = value, this.requestUpdate());
22
+ });
23
+
24
+ /** @type {number} */
25
+ this.bandwidth = handleParam(bandwidth, value => {
26
+ this.bandwidth = value;
27
+ return this.grid ? this.convolve().update() : null;
21
28
  });
22
29
  }
23
30
 
@@ -39,7 +46,8 @@ export class Density1DMark extends Mark {
39
46
  }
40
47
 
41
48
  queryResult(data) {
42
- this.grid = grid1d(this.bins, data);
49
+ const { columns: { index, density } } = toDataColumns(data);
50
+ this.grid = grid1d(this.bins, index, density);
43
51
  return this.convolve();
44
52
  }
45
53
 
@@ -53,29 +61,30 @@ export class Density1DMark extends Mark {
53
61
  const result = dericheConv1d(config, grid, bins);
54
62
 
55
63
  // map smoothed grid values to sample data points
56
- const points = this.data = [];
57
64
  const v = dim === 'x' ? 'y' : 'x';
58
65
  const b = this.channelField(dim).as;
59
66
  const b0 = +lo;
60
67
  const delta = (hi - b0) / (bins - 1);
61
68
  const scale = 1 / delta;
69
+
70
+ const _b = new Float64Array(bins);
71
+ const _v = new Float64Array(bins);
62
72
  for (let i = 0; i < bins; ++i) {
63
- points.push({
64
- [b]: b0 + i * delta,
65
- [v]: result[i] * scale
66
- });
73
+ _b[i] = b0 + i * delta;
74
+ _v[i] = result[i] * scale;
67
75
  }
76
+ this.data = { numRows: bins, columns: { [b]: _b, [v]: _v } };
68
77
 
69
78
  return this;
70
79
  }
71
80
 
72
81
  plotSpecs() {
73
- const { type, data, channels, dim } = this;
74
- const options = dim === 'x' ? { y: 'y' } : { x: 'x' };
82
+ const { type, data: { numRows: length, columns }, channels, dim } = this;
83
+ const options = dim === 'x' ? { y: columns.y } : { x: columns.x };
75
84
  for (const c of channels) {
76
- options[c.channel] = channelOption(c);
85
+ options[c.channel] = channelOption(c, columns);
77
86
  }
78
- return [{ type, data, options }];
87
+ return [{ type, data: { length }, options }];
79
88
  }
80
89
  }
81
90