Skip to content

Commit

Permalink
feat(chart): debounce chart.render (#5083)
Browse files Browse the repository at this point in the history
  • Loading branch information
pearmini authored May 24, 2023
1 parent e18cc84 commit 106b807
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { chartRenderClearAnimation as render } from '../plots/api/chart-render-c
import { createNodeGCanvas } from './utils/createNodeGCanvas';
import './utils/useSnapshotMatchers';

describe('chart.render', () => {
// Skip it, because debounce of chart.render.
describe.skip('chart.render', () => {
const canvas = createNodeGCanvas(640, 480);

it('chart.render should clear prev animation', async () => {
Expand Down
99 changes: 93 additions & 6 deletions __tests__/unit/api/chart.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Canvas } from '@antv/g';
import { Renderer as SVGRenderer } from '@antv/g-svg';
import { Chart, createLibrary } from '../../../src';
import { Chart, createLibrary, ChartEvent } from '../../../src';
import {
View,
TimingKeyframe,
Expand Down Expand Up @@ -40,13 +40,32 @@ import {
Gauge,
} from '../../../src/api/mark/mark';

const TEST_OPTIONS = {
type: 'interval',
theme: 'classic',
encode: { x: 'genre', y: 'sold' },
data: [
{ genre: 'Sports', sold: 275 },
{ genre: 'Strategy', sold: 115 },
{ genre: 'Action', sold: 120 },
{ genre: 'Shooter', sold: 350 },
{ genre: 'Other', sold: 150 },
],
};

describe('Chart', () => {
it('Chart() should have expected defaults.', () => {
const chart = new Chart({ theme: 'classic' });
expect(chart.type).toBe('view');
expect(chart.parentNode).toBeNull();
expect(chart.value).toEqual({ theme: 'classic', key: undefined });
expect(chart['_container'].nodeName).toBe('DIV');
expect(chart['_trailing']).toBe(false);
expect(chart['_rendering']).toBe(false);
expect(chart['_plugins'].length).toBe(0);
expect(chart['_renderer']).toBeDefined();
expect(chart['_trailingResolve']).toBeNull();
expect(chart['_trailingReject']).toBeNull();
});

it('Chart({...}) should support HTML container.', () => {
Expand Down Expand Up @@ -498,12 +517,13 @@ describe('Chart', () => {
fn();
return render();
};
chart.render();

// Mock resize window.
div.style.width = '100px';
div.style.height = '100px';
window.dispatchEvent(new Event('resize'));
chart.render().then(() => {
// Mock resize window.
div.style.width = '100px';
div.style.height = '100px';
window.dispatchEvent(new Event('resize'));
});

// Listen.
chart.on('afterchangesize', () => {
Expand Down Expand Up @@ -718,4 +738,71 @@ describe('Chart', () => {
await chart.forceFit();
expect(fn).toBeCalledTimes(1);
});

it('chart.render() should toggle value of _rendering.', async () => {
const chart = new Chart({});

chart.options(TEST_OPTIONS);

const finished = chart.render();
expect(chart['_rendering']).toBeTruthy();

await finished;
expect(chart['_rendering']).toBeFalsy();
});

it('chart.render() should catch error for trailing render task.', async () => {
const chart = new Chart({});
chart.options(TEST_OPTIONS);
chart.render();

chart.options({ ...TEST_OPTIONS, theme: 'foo' });
await expect(chart.render()).rejects.toThrowError();
expect(chart['_rendering']).toBeFalsy();
});

it('chart.render() should render after previous rendering.', async () => {
const chart = new Chart({});

chart.options(TEST_OPTIONS);

let count = 0;
chart.on(ChartEvent.BEFORE_RENDER, () => {
count++;
});
const p1 = chart.render();
const p2 = chart.render();

await p1;
expect(count).toBe(1);
await p2;
expect(count).toBe(2);
});

it('chart.render() should render first and last rendering task in a row.', async () => {
const chart = new Chart({});

chart.options(TEST_OPTIONS);

let count = 0;
chart.on(ChartEvent.AFTER_RENDER, () => {
count++;
});

const p1 = chart.render();
const p2 = chart.render();
const p3 = chart.render();
const p4 = chart.render();

const v1 = await p1;
const v2 = await p2;
const v3 = await p3;
const v4 = await p4;

expect(count).toBe(2);
expect(v1).toBe(chart);
expect(v2).toBe(chart);
expect(v3).toBe(chart);
expect(v4).toBe(chart);
});
});
131 changes: 92 additions & 39 deletions src/api/chart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
sizeOf,
optionsOf,
updateRoot,
VIEW_KEYS,
createEmptyPromise,
} from './utils';

export const G2_CHART_KEY = 'G2_CHART_KEY';
Expand Down Expand Up @@ -92,6 +92,10 @@ export class Chart extends View<ChartOptions> {
private _plugins: RendererPlugin[];
// Identifies whether bindAutoFit.
private _hasBindAutoFit = false;
private _rendering = false;
private _trailing = false;
private _trailingResolve = null;
private _trailingReject = null;

constructor(options: ChartOptions) {
const { container, canvas, renderer, plugins, ...rest } = options || {};
Expand All @@ -104,45 +108,24 @@ export class Chart extends View<ChartOptions> {
}

render(): Promise<Chart> {
if (this._rendering) return this._addToTrailing();
if (!this._context.canvas) this._createCanvas();
this._bindAutoFit();

if (!this._context.canvas) {
// Init width and height.
const { width, height } = sizeOf(this.options(), this._container);
// Create canvas if it does not exist.
// DragAndDropPlugin is for interaction.
// It is OK to register more than one time, G will handle this.
this._plugins.push(new DragAndDropPlugin());
this._plugins.forEach((d) => this._renderer.registerPlugin(d));
this._context.canvas = new GCanvas({
container: this._container,
width,
height,
renderer: this._renderer,
});
}

return new Promise((resolve, reject) => {
try {
const options = this.options();
const { key = G2_CHART_KEY } = options;
const { width, height } = sizeOf(options, this._container);

// Update actual size and key.
this._width = width;
this._height = height;
this._key = key;

render(
{ key: this._key, ...options, width, height },
this._context,
() => resolve(this),
reject,
);
} catch (e) {
reject(e);
}
});
this._rendering = true;
const finished = new Promise<Chart>((resolve, reject) =>
render(
this._computedOptions(),
this._context,
this._createResolve(resolve),
this._createReject(reject),
),
);
const [finished1, resolve, reject] = createEmptyPromise<Chart>();
finished
.then(resolve)
.catch(reject)
.then(() => this._renderTrailing());
return finished1;
}

/**
Expand Down Expand Up @@ -250,6 +233,76 @@ export class Chart extends View<ChartOptions> {
this.children = [];
}

private _renderTrailing() {
if (!this._trailing) return;
this._trailing = false;
this.render()
.then(() => {
const trailingResolve = this._trailingResolve.bind(this);
this._trailingResolve = null;
trailingResolve(this);
})
.catch((error) => {
const trailingReject = this._trailingReject.bind(this);
this._trailingReject = null;
trailingReject(error);
});
}

private _createResolve(resolve: (chart: Chart) => void) {
return () => {
this._rendering = false;
resolve(this);
};
}

private _createReject(reject: (error: Error) => void) {
return (error: Error) => {
this._rendering = false;
reject(error);
};
}

// Update actual size and key.
private _computedOptions() {
const options = this.options();
const { key = G2_CHART_KEY } = options;
const { width, height } = sizeOf(options, this._container);
this._width = width;
this._height = height;
this._key = key;
return { key: this._key, ...options, width, height };
}

// Create canvas if it does not exist.
// DragAndDropPlugin is for interaction.
// It is OK to register more than one time, G will handle this.
private _createCanvas() {
const { width, height } = sizeOf(this.options(), this._container);
this._plugins.push(new DragAndDropPlugin());
this._plugins.forEach((d) => this._renderer.registerPlugin(d));
this._context.canvas = new GCanvas({
container: this._container,
width,
height,
renderer: this._renderer,
});
}

private _addToTrailing(): Promise<Chart> {
// Resolve previous promise, and give up this task.
this._trailingResolve?.(this);

// Create new task.
this._trailing = true;
const promise = new Promise<Chart>((resolve, reject) => {
this._trailingResolve = resolve;
this._trailingReject = reject;
});

return promise;
}

private _onResize = debounce(() => {
this.forceFit();
}, 300);
Expand Down
14 changes: 14 additions & 0 deletions src/api/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,17 @@ export function updateRoot(node: Node, options: G2ViewTree) {
}
}
}

export function createEmptyPromise<T>(): [
Promise<T>,
(reason?: any) => void,
(value: T | PromiseLike<T>) => void,
] {
let reject: (reason?: any) => void;
let resolve: (value: T | PromiseLike<T>) => void;
const cloned = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return [cloned, resolve, reject];
}

0 comments on commit 106b807

Please sign in to comment.