Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Plot status events to vgplot #157

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions docs/api/vgplot/plot.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,25 @@ Return the "inner" width of the plot, which is the `width` attribute value minus

Return the "inner" height of the plot, which is the `height` attribute value minus the `topMargin` and `bottomMargin` values.

### status

`plot.status`

Return the `status` of a `Plot` instance (one of `idle`, `pendingQuery` and `pendingRender`).

- Initial `status` is `idle`.
- When marks are initially connected to coordinator (via `plot.connect()`) or when a mark of a plot has a pending query, status will change to `pendingQuery`.
- When all pending queries of plot marks are done, status will change to `pendingRender`.
- After rendering, status will change to `idle` again.

There is a `status` event listener available, see `plot.addEventListener` and `plot.removeEventListener`.

### connect

`plot.connect()`

Connect all [`Mark`](./marks) instances to coordinator.

### pending

`plot.pending(mark)`
Expand Down Expand Up @@ -94,6 +113,12 @@ Adds an event listener _callback_ that is invoked when the attribute with the gi

Removes an event listener _callback_ associated with the given attribute _name_.

### addDirectives

`plot.addDirectives(directives)`

Adds directives to plot.

### addParams

`plot.addParams(mark, paramSet)`
Expand Down Expand Up @@ -128,3 +153,16 @@ Called by [interactor directives](./interactors).
Add a _legend_ associated with this plot.
The _include_ flag (default `true`) indicates if the legend should be included within the same container element as the plot.
Called by [legend directives](./legends).

### addEventListener

`plot.addEventListener(type, callback)`

Add an event listener _callback_ function for the specified event _type_.
`Plot` supports `"status"` type events only.

### removeEventListener

`plot.removeEventListener(type, callback)`

Remove an event listener _callback_ function for the specified event _type_.
8 changes: 8 additions & 0 deletions docs/api/vgplot/specs.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,14 @@ The supported external _options_ are:
- _params_: An array (default `[]`) of predefined [`Param`](../core/param) instances. Each entry should have the form `[name, param]`.
- _datasets_: An array (default `[]`) of preloaded browser-managed datasets (such as GeoJSON data). Each entry should have the form `[name, dataset]`.

## parsePlotSpec

`parsePlotSpec(specification, options, element)`

Parse a JSON _specification_ of a single plot and return corresponding `Plot` instance.
See `parseSpec` for supported _options_.
If provided, the input _element_ will be used as the container for the plot, otherwise a new `div` element will be generated.

## specToModule

`specToModule(spec, options)`
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export { wasmConnector } from './connectors/wasm.js';
export { distinct } from './util/distinct.js';
export { synchronizer } from './util/synchronizer.js';
export { throttle } from './util/throttle.js';
export { AsyncDispatch } from './util/AsyncDispatch.js';
6 changes: 4 additions & 2 deletions packages/vgplot/src/directives/plot.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { Plot } from '../plot.js';

export function plot(...directives) {
const p = new Plot();
directives.flat().forEach(dir => dir(p));
p.marks.forEach(mark => coordinator().connect(mark));

p.addDirectives(directives.flat());
p.connect();

return p.element;
}

Expand Down
1 change: 1 addition & 0 deletions packages/vgplot/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ export {

export {
parseSpec,
parsePlotSpec,
ParseContext
} from './spec/parse-spec.js';

Expand Down
26 changes: 24 additions & 2 deletions packages/vgplot/src/plot.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { distinct, synchronizer } from '@uwdata/mosaic-core';
import { coordinator, distinct, synchronizer, AsyncDispatch } from '@uwdata/mosaic-core';
import { plotRenderer } from './plot-renderer.js';

const DEFAULT_ATTRIBUTES = {
Expand All @@ -9,8 +9,10 @@ const DEFAULT_ATTRIBUTES = {
marginBottom: 30
};

export class Plot {
export class Plot extends AsyncDispatch {
constructor(element) {
super();

this.attributes = { ...DEFAULT_ATTRIBUTES };
this.listeners = null;
this.interactors = [];
Expand All @@ -23,6 +25,7 @@ export class Plot {
this.element.value = this;
this.params = new Map;
this.synch = synchronizer();
this.status = 'idle';
}

margins() {
Expand All @@ -44,13 +47,22 @@ export class Plot {
return this.getAttribute('height') - top - bottom;
}

async connect() {
this.updateStatus('pendingQuery');

await Promise.all(this.marks.map(mark => coordinator().connect(mark)));
}

pending(mark) {
if (this.status !== 'pendingQuery') this.updateStatus('pendingQuery');

this.synch.pending(mark);
}

update(mark) {
if (this.synch.ready(mark) && !this.pendingRender) {
this.pendingRender = true;
this.updateStatus('pendingRender');
requestAnimationFrame(() => this.render());
}
return this.synch.promise;
Expand All @@ -65,6 +77,7 @@ export class Plot {
});
this.element.replaceChildren(svg, ...legends);
this.synch.resolve();
this.updateStatus('idle');
}

getAttribute(name) {
Expand Down Expand Up @@ -97,6 +110,10 @@ export class Plot {
return this.listeners?.get(name)?.delete(callback);
}

addDirectives(directives) {
directives.forEach(dir => dir(this));
}

addParams(mark, paramSet) {
const { params } = this;
for (const param of paramSet) {
Expand Down Expand Up @@ -133,4 +150,9 @@ export class Plot {
legend.setPlot(this);
this.legends.push({ legend, include });
}

updateStatus(status) {
this.status = status;
this.emit('status', this.status);
}
}
34 changes: 31 additions & 3 deletions packages/vgplot/src/spec/parse-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ import { hconcat, vconcat, hspace, vspace } from '../layout/index.js';
import { parse as isoparse } from 'isoformat';

import { from } from '../directives/data.js';
import { plot as _plot } from '../directives/plot.js';
import * as marks from '../directives/marks.js';
import * as legends from '../directives/legends.js';
import * as attributes from '../directives/attributes.js';
import * as interactors from '../directives/interactors.js';
import { Fixed } from '../symbols.js';
import { Plot } from '../plot.js';

import {
parseData, parseCSVData, parseJSONData,
Expand Down Expand Up @@ -99,6 +99,22 @@ export function parseSpec(spec, options) {
return new ParseContext(options).parse(spec);
}

export function parsePlotSpec(spec, options, element) {
spec = isString(spec) ? JSON.parse(spec) : spec;

if (!('plot' in spec))
throw new Error('Plot spec requires a "plot" property.');

const parsePlot = (spec, ctx) => parsePlotInstance(spec, ctx, element);

return new ParseContext({
...options,
specParsers: new Map([
['plot', { type: isArray, parse: parsePlot }]
])
}).parse(spec);
}

export class ParseContext {
constructor({
specParsers = DefaultSpecParsers,
Expand Down Expand Up @@ -273,13 +289,25 @@ function parseHConcat(spec, ctx) {
return hconcat(spec.hconcat.map(s => parseComponent(s, ctx)));
}

function parsePlot(spec, ctx) {
function parsePlotInstance(spec, ctx, element) {
const { plot, ...attributes } = spec;
const attrs = ctx.plotDefaults.concat(
Object.keys(attributes).map(key => parseAttribute(spec, key, ctx))
);
const entries = plot.map(e => parseEntry(e, ctx));
return _plot(attrs, entries);

const p = new Plot(element);
p.addDirectives([...attrs, ...entries]);

return p;
}

function parsePlot(spec, ctx) {
const p = parsePlotInstance(spec, ctx);

p.connect();

return p.element;
}

function parseNakedMark(spec, ctx) {
Expand Down