diff --git a/__tests__/integration/api-chart-render-update-attribute.spec.ts b/__tests__/integration/api-chart-render-update-attribute.spec.ts new file mode 100644 index 0000000000..692c72e454 --- /dev/null +++ b/__tests__/integration/api-chart-render-update-attribute.spec.ts @@ -0,0 +1,32 @@ +import { chartRenderUpdateAttributes as render } from '../plots/api/chart-render-update-attributes'; +import { createNodeGCanvas } from './utils/createNodeGCanvas'; +import { kebabCase } from './utils/kebabCase'; +import './utils/useCustomFetch'; +import './utils/useSnapshotMatchers'; + +describe('chart.render', () => { + const dir = `${__dirname}/snapshots/api/${kebabCase(render.name)}`; + const canvas = createNodeGCanvas(800, 500); + + it('chart.render() should update attribute without animation', async () => { + const { finished, chart, refreshed, refreshed1, button, ...rest } = render({ + canvas, + container: document.createElement('div'), + }); + await finished; + + // To lineDash + button.dispatchEvent(new CustomEvent('click')); + await refreshed; + await expect(canvas).toMatchCanvasSnapshot(dir, 'step0'); + + // Reset + button.dispatchEvent(new CustomEvent('click')); + await refreshed1; + await expect(canvas).toMatchCanvasSnapshot(dir, 'step1'); + }); + + afterAll(() => { + canvas?.destroy(); + }); +}); diff --git a/__tests__/integration/api-chart-render-update-non-animation.spec.ts b/__tests__/integration/api-chart-render-update-non-animation.spec.ts new file mode 100644 index 0000000000..8679fd7c94 --- /dev/null +++ b/__tests__/integration/api-chart-render-update-non-animation.spec.ts @@ -0,0 +1,26 @@ +import { chartRenderUpdateNonAnimation as render } from '../plots/api/chart-render-update-non-animation'; +import { createNodeGCanvas } from './utils/createNodeGCanvas'; +import { kebabCase } from './utils/kebabCase'; +import './utils/useCustomFetch'; +import './utils/useSnapshotMatchers'; + +describe('chart.render', () => { + const dir = `${__dirname}/snapshots/api/${kebabCase(render.name)}`; + const canvas = createNodeGCanvas(800, 500); + + it('chart.render() should update non animation node.', async () => { + const { finished, chart, refreshed, button, ...rest } = render({ + canvas, + container: document.createElement('div'), + }); + await finished; + + button.dispatchEvent(new CustomEvent('click')); + await refreshed; + await expect(canvas).toMatchCanvasSnapshot(dir, 'step0'); + }); + + afterAll(() => { + canvas?.destroy(); + }); +}); diff --git a/__tests__/integration/snapshots/api/chart-render-update-attributes/step0.png b/__tests__/integration/snapshots/api/chart-render-update-attributes/step0.png new file mode 100644 index 0000000000..87734c4bf3 Binary files /dev/null and b/__tests__/integration/snapshots/api/chart-render-update-attributes/step0.png differ diff --git a/__tests__/integration/snapshots/api/chart-render-update-attributes/step1.png b/__tests__/integration/snapshots/api/chart-render-update-attributes/step1.png new file mode 100644 index 0000000000..7d0ed3b823 Binary files /dev/null and b/__tests__/integration/snapshots/api/chart-render-update-attributes/step1.png differ diff --git a/__tests__/integration/snapshots/api/chart-render-update-non-animation/step0.png b/__tests__/integration/snapshots/api/chart-render-update-non-animation/step0.png new file mode 100644 index 0000000000..fa0d4c1667 Binary files /dev/null and b/__tests__/integration/snapshots/api/chart-render-update-non-animation/step0.png differ diff --git a/__tests__/plots/api/chart-render-update-attributes.ts b/__tests__/plots/api/chart-render-update-attributes.ts new file mode 100644 index 0000000000..2929e85927 --- /dev/null +++ b/__tests__/plots/api/chart-render-update-attributes.ts @@ -0,0 +1,68 @@ +import { Chart } from '../../../src'; + +export function chartRenderUpdateAttributes(context) { + const { container, canvas } = context; + + // button + const button = document.createElement('button'); + button.innerText = 'Rerender'; + container.appendChild(button); + + // wrapperDiv + const wrapperDiv = document.createElement('div'); + container.appendChild(wrapperDiv); + + const chart = new Chart({ + theme: 'classic', + container: wrapperDiv, + canvas, + }); + + const options = { + type: 'line', + data: { + type: 'fetch', + value: 'data/aapl.csv', + transform: [{ type: 'slice', start: 0, end: 10 }], + }, + encode: { + x: 'date', + y: 'close', + }, + axis: { x: false }, + }; + + chart.options(options); + + const finished = chart.render(); + + let resolve; + let resolve1; + const refreshed = new Promise((r) => (resolve = r)); + const refreshed1 = new Promise((r) => (resolve1 = r)); + + let lineDash = false; + button.onclick = () => { + if (lineDash) { + chart.options({ + ...options, + style: { + lineDash: null, + }, + }); + lineDash = false; + chart.render().then(resolve1); + } else { + chart.options({ + ...options, + style: { + lineDash: [5, 4], + }, + }); + lineDash = true; + chart.render().then(resolve); + } + }; + + return { chart, button, finished, refreshed, refreshed1 }; +} diff --git a/__tests__/plots/api/chart-render-update-non-animation.ts b/__tests__/plots/api/chart-render-update-non-animation.ts new file mode 100644 index 0000000000..7f86c7df68 --- /dev/null +++ b/__tests__/plots/api/chart-render-update-non-animation.ts @@ -0,0 +1,53 @@ +import { Chart } from '../../../src'; + +export function chartRenderUpdateNonAnimation(context) { + const { container, canvas } = context; + + // button + const button = document.createElement('button'); + button.innerText = 'Rerender'; + container.appendChild(button); + + // wrapperDiv + const wrapperDiv = document.createElement('div'); + container.appendChild(wrapperDiv); + + const chart = new Chart({ + theme: 'classic', + container: wrapperDiv, + canvas, + }); + + const options = { + type: 'interval', + data: [ + { genre: 'Sports', sold: 275 }, + { genre: 'Strategy', sold: 115 }, + { genre: 'Action', sold: 120 }, + { genre: 'Shooter', sold: 350 }, + { genre: 'Other', sold: 150 }, + ], + encode: { + x: 'genre', + y: 'sold', + }, + animate: false, + }; + + chart.options(options); + + const finished = chart.render(); + + let resolve; + const refreshed = new Promise((r) => (resolve = r)); + + button.onclick = () => { + chart.options({ + ...options, + type: 'point', + }); + chart.render().then(resolve); + }; + + return { chart, button, finished, refreshed }; +} diff --git a/__tests__/plots/api/index.ts b/__tests__/plots/api/index.ts index cdff32ca93..0d908f12d6 100644 --- a/__tests__/plots/api/index.ts +++ b/__tests__/plots/api/index.ts @@ -20,3 +20,5 @@ export { chartOnFocusContext } from './chart-on-focus-context'; export { chartEmitItemTooltip } from './chart-emit-item-tooltip'; export { chartEmitSeriesTooltip } from './chart-emit-series-tooltip'; export { chartEmitPieTooltip } from './chart-emit-pie-tooltip'; +export { chartRenderUpdateAttributes } from './chart-render-update-attributes'; +export { chartRenderUpdateNonAnimation } from './chart-render-update-non-animation'; diff --git a/src/animation/morphing.ts b/src/animation/morphing.ts index 66a8a84ffd..9cbf34a595 100644 --- a/src/animation/morphing.ts +++ b/src/animation/morphing.ts @@ -5,6 +5,7 @@ import { Path, } from '@antv/g'; import { AnimationComponent as AC } from '../runtime'; +import { copyAttributes } from '../utils/helper'; import { Animation } from './types'; import { attributeKeys, attributeOf, effectTiming } from './utils'; @@ -181,6 +182,10 @@ function oneToOne( ]; const animation = pathShape.animate(keyframes, timeEffect); + animation.onfinish = () => { + copyAttributes(pathShape, to); + }; + // Remove transform because it already applied in path // converted by convertToPath. // @todo Remove this scale(1, 1) diff --git a/src/runtime/plot.ts b/src/runtime/plot.ts index bd3226f45f..ddfa7671ff 100644 --- a/src/runtime/plot.ts +++ b/src/runtime/plot.ts @@ -823,7 +823,10 @@ async function plotView( maybeFacetElement(this, parent, origin); const node = shapeFunction(data, index); const animation = updateFunction(data, [this], [node]); - if (animation === null) copyAttributes(this, node); + if (animation === null) { + if (this.nodeName === node.nodeName) copyAttributes(this, node); + else this.parentNode.replaceChild(node, this); + } return animation; }); }), diff --git a/src/utils/helper.ts b/src/utils/helper.ts index 3a71ed4d48..b2036477a2 100644 --- a/src/utils/helper.ts +++ b/src/utils/helper.ts @@ -1,5 +1,5 @@ import { DisplayObject } from '@antv/g'; -import { lowerFirst, startsWith, upperFirst } from '@antv/util'; +import { lowerFirst, upperFirst } from '@antv/util'; export function identity(x: T): T { return x;