From e17558894569c45d53c2ed77e79cc3da9be1adbe Mon Sep 17 00:00:00 2001 From: MiniPear Date: Tue, 8 Aug 2023 18:02:49 +0800 Subject: [PATCH] refactor(api): auto generate marks from stdlib --- __tests__/integration/utils/renderSpec.ts | 3 +- __tests__/main.ts | 5 +- .../plots/api/chart-options-composite-mark.ts | 1 + __tests__/unit/api/chart.spec.ts | 129 ++--- __tests__/unit/api/composition.spec.ts | 145 +++--- __tests__/unit/api/mark.spec.ts | 183 +++----- __tests__/unit/api/options.spec.ts | 11 +- __tests__/unit/api/props.spec.ts | 22 +- __tests__/utils/renderToMountedElement.ts | 4 +- src/api/chart.ts | 347 +------------- .../{composition/base.ts => composition.ts} | 24 +- src/api/composition/facetCircle.ts | 36 -- src/api/composition/facetRect.ts | 36 -- src/api/composition/geoPath.ts | 37 -- src/api/composition/geoView.ts | 39 -- src/api/composition/index.ts | 57 --- src/api/composition/repeatMatrix.ts | 36 -- src/api/composition/spaceFlex.ts | 18 - src/api/composition/spaceLayer.ts | 18 - src/api/composition/timingKeyframe.ts | 16 - src/api/composition/view.ts | 42 -- src/api/define.ts | 128 +++++ src/api/extend.ts | 31 ++ src/api/{mark/base.ts => mark.ts} | 17 +- src/api/mark/index.ts | 113 ----- src/api/mark/mark.ts | 439 ------------------ src/api/mark/types.ts | 31 -- src/api/node.ts | 6 +- src/api/props.ts | 144 ++---- src/api/runtime.ts | 362 +++++++++++++++ src/api/types.ts | 112 ++++- src/api/utils.ts | 39 +- src/runtime/index.ts | 2 +- src/runtime/render.ts | 9 +- src/runtime/types/component.ts | 2 +- src/runtime/types/options.ts | 2 +- src/spec/composition.ts | 7 + src/spec/index.ts | 1 + src/spec/interaction.ts | 2 + src/stdlib/index.ts | 5 +- 40 files changed, 914 insertions(+), 1747 deletions(-) rename src/api/{composition/base.ts => composition.ts} (72%) delete mode 100644 src/api/composition/facetCircle.ts delete mode 100644 src/api/composition/facetRect.ts delete mode 100644 src/api/composition/geoPath.ts delete mode 100644 src/api/composition/geoView.ts delete mode 100644 src/api/composition/index.ts delete mode 100644 src/api/composition/repeatMatrix.ts delete mode 100644 src/api/composition/spaceFlex.ts delete mode 100644 src/api/composition/spaceLayer.ts delete mode 100644 src/api/composition/timingKeyframe.ts delete mode 100644 src/api/composition/view.ts create mode 100644 src/api/define.ts create mode 100644 src/api/extend.ts rename src/api/{mark/base.ts => mark.ts} (75%) delete mode 100644 src/api/mark/index.ts delete mode 100644 src/api/mark/mark.ts delete mode 100644 src/api/mark/types.ts create mode 100644 src/api/runtime.ts diff --git a/__tests__/integration/utils/renderSpec.ts b/__tests__/integration/utils/renderSpec.ts index 1b67c132ac..8a7e26b71c 100644 --- a/__tests__/integration/utils/renderSpec.ts +++ b/__tests__/integration/utils/renderSpec.ts @@ -1,6 +1,6 @@ import { Canvas } from '@antv/g'; import { createCanvas } from 'canvas'; -import { G2Context, G2Spec, render } from '../../../src'; +import { G2Context, G2Spec, createLibrary, render } from '../../../src'; import { renderToMountedElement } from '../../utils/renderToMountedElement'; import { createNodeGCanvas } from './createNodeGCanvas'; @@ -15,6 +15,7 @@ export async function renderSpec( const renderFunction = mounted ? renderToMountedElement : render; const options = preprocess({ ...raw, width, height }); context.canvas = gCanvas; + context.library = createLibrary(); context.createCanvas = () => { // The width attribute defaults to 300, and the height attribute defaults to 150. // @see https://stackoverflow.com/a/12019582 diff --git a/__tests__/main.ts b/__tests__/main.ts index 58080fe7b8..e6812789f3 100644 --- a/__tests__/main.ts +++ b/__tests__/main.ts @@ -5,7 +5,7 @@ import { Plugin as ControlPlugin } from '@antv/g-plugin-control'; import { Plugin as ThreeDPlugin } from '@antv/g-plugin-3d'; import { Renderer as SVGRenderer } from '@antv/g-svg'; import { Renderer as WebGLRenderer } from '@antv/g-webgl'; -import { render } from '../src'; +import { createLibrary, render } from '../src'; import { renderToMountedElement } from './utils/renderToMountedElement'; import * as statics from './plots/static'; import * as interactions from './plots/interaction'; @@ -168,7 +168,8 @@ function createSpecRender(object) { before?.(); const node = renderChart( { theme: 'classic', ...options }, - { canvas }, + // @ts-ignore + { canvas, library: createLibrary() }, () => after?.(), ); diff --git a/__tests__/plots/api/chart-options-composite-mark.ts b/__tests__/plots/api/chart-options-composite-mark.ts index 6c8cc0f0fe..583f837ce1 100644 --- a/__tests__/plots/api/chart-options-composite-mark.ts +++ b/__tests__/plots/api/chart-options-composite-mark.ts @@ -29,6 +29,7 @@ export function chartOptionsCompositeMark(context) { { genre: 'Shooter', sold: 350 }, { genre: 'Other', sold: 150 }, ], + // @ts-ignore encode: { value: 'sold', color: 'genre' }, }); diff --git a/__tests__/unit/api/chart.spec.ts b/__tests__/unit/api/chart.spec.ts index 463459487e..21ec000492 100644 --- a/__tests__/unit/api/chart.spec.ts +++ b/__tests__/unit/api/chart.spec.ts @@ -1,49 +1,6 @@ import { Canvas } from '@antv/g'; import { Renderer as SVGRenderer } from '@antv/g-svg'; import { Chart, createLibrary, ChartEvent } from '../../../src'; -import { - View, - TimingKeyframe, - SpaceFlex, - FacetRect, - RepeatMatrix, - FacetCircle, - SpaceLayer, -} from '../../../src/api/composition'; -import { - Area, - Cell, - Image, - Interval, - Line, - Link, - Point, - Polygon, - Vector, - Text, - Box, - LineX, - LineY, - Range, - RangeX, - RangeY, - Rect, - Connector, - Boxplot, - Sankey, - Treemap, - Shape, - Pack, - ForceGraph, - Tree, - WordCloud, - Gauge, - Density, - Heatmap, - AxisX, - AxisY, - Legends, -} from '../../../src/api/mark/mark'; const TEST_OPTIONS = { type: 'interval', @@ -140,39 +97,39 @@ describe('Chart', () => { it('chart.nodeName() should return expected node.', () => { const chart = new Chart({ theme: 'classic' }); - expect(chart.interval()).toBeInstanceOf(Interval); - expect(chart.rect()).toBeInstanceOf(Rect); - expect(chart.point()).toBeInstanceOf(Point); - expect(chart.area()).toBeInstanceOf(Area); - expect(chart.line()).toBeInstanceOf(Line); - expect(chart.cell()).toBeInstanceOf(Cell); - expect(chart.vector()).toBeInstanceOf(Vector); - expect(chart.link()).toBeInstanceOf(Link); - expect(chart.polygon()).toBeInstanceOf(Polygon); - expect(chart.image()).toBeInstanceOf(Image); - expect(chart.text()).toBeInstanceOf(Text); - expect(chart.box()).toBeInstanceOf(Box); - expect(chart.lineX()).toBeInstanceOf(LineX); - expect(chart.lineY()).toBeInstanceOf(LineY); - expect(chart.range()).toBeInstanceOf(Range); - expect(chart.rangeX()).toBeInstanceOf(RangeX); - expect(chart.rangeY()).toBeInstanceOf(RangeY); - expect(chart.connector()).toBeInstanceOf(Connector); - expect(chart.sankey()).toBeInstanceOf(Sankey); - expect(chart.treemap()).toBeInstanceOf(Treemap); - expect(chart.boxplot()).toBeInstanceOf(Boxplot); - expect(chart.shape()).toBeInstanceOf(Shape); - expect(chart.pack()).toBeInstanceOf(Pack); - expect(chart.forceGraph()).toBeInstanceOf(ForceGraph); - expect(chart.tree()).toBeInstanceOf(Tree); - expect(chart.wordCloud()).toBeInstanceOf(WordCloud); - expect(chart.gauge()).toBeInstanceOf(Gauge); - expect(chart.density()).toBeInstanceOf(Density); - expect(chart.heatmap()).toBeInstanceOf(Heatmap); - expect(chart.axisX()).toBeInstanceOf(AxisX); - expect(chart.axisY()).toBeInstanceOf(AxisY); - expect(chart.legends()).toBeInstanceOf(Legends); - expect(chart.options().children).toEqual([ + expect(chart.interval().type).toBe('interval'); + expect(chart.rect().type).toBe('rect'); + expect(chart.point().type).toBe('point'); + expect(chart.area().type).toBe('area'); + expect(chart.line().type).toBe('line'); + expect(chart.cell().type).toBe('cell'); + expect(chart.vector().type).toBe('vector'); + expect(chart.link().type).toBe('link'); + expect(chart.polygon().type).toBe('polygon'); + expect(chart.image().type).toBe('image'); + expect(chart.text().type).toBe('text'); + expect(chart.box().type).toBe('box'); + expect(chart.lineX().type).toBe('lineX'); + expect(chart.lineY().type).toBe('lineY'); + expect(chart.range().type).toBe('range'); + expect(chart.rangeX().type).toBe('rangeX'); + expect(chart.rangeY().type).toBe('rangeY'); + expect(chart.connector().type).toBe('connector'); + expect(chart.sankey().type).toBe('sankey'); + expect(chart.treemap().type).toBe('treemap'); + expect(chart.boxplot().type).toBe('boxplot'); + expect(chart.shape().type).toBe('shape'); + expect(chart.pack().type).toBe('pack'); + expect(chart.forceGraph().type).toBe('forceGraph'); + expect(chart.tree().type).toBe('tree'); + expect(chart.wordCloud().type).toBe('wordCloud'); + expect(chart.gauge().type).toBe('gauge'); + expect(chart.density().type).toBe('density'); + expect(chart.heatmap().type).toBe('heatmap'); + expect(chart.axisX().type).toBe('axisX'); + expect(chart.axisY().type).toBe('axisY'); + expect(chart.legends().type).toBe('legends'); + expect((chart.options() as any).children).toEqual([ { type: 'interval' }, { type: 'rect' }, { type: 'point' }, @@ -212,7 +169,7 @@ describe('Chart', () => { const chart = new Chart({ theme: 'classic' }); chart.view(); chart.spaceLayer(); - expect(chart.spaceLayer()).toBeInstanceOf(SpaceLayer); + expect(chart.spaceLayer().type).toBe('spaceLayer'); }); it('chart.container() should set layout options for root node.', () => { @@ -264,19 +221,19 @@ describe('Chart', () => { it('chart.container() should return expected container.', () => { const chart = new Chart({ theme: 'classic' }); - expect(chart.view()).toBeInstanceOf(View); + expect(chart.view().type).toBe('view'); expect(chart.options()).toEqual({ type: 'view', theme: 'classic' }); - expect(chart.spaceLayer()).toBeInstanceOf(SpaceLayer); + expect(chart.spaceLayer().type).toBe('spaceLayer'); expect(chart.options()).toEqual({ type: 'spaceLayer', theme: 'classic' }); - expect(chart.spaceFlex()).toBeInstanceOf(SpaceFlex); + expect(chart.spaceFlex().type).toBe('spaceFlex'); expect(chart.options()).toEqual({ type: 'spaceFlex', theme: 'classic' }); - expect(chart.facetRect()).toBeInstanceOf(FacetRect); + expect(chart.facetRect().type).toBe('facetRect'); expect(chart.options()).toEqual({ type: 'facetRect', theme: 'classic' }); - expect(chart.repeatMatrix()).toBeInstanceOf(RepeatMatrix); + expect(chart.repeatMatrix().type).toBe('repeatMatrix'); expect(chart.options()).toEqual({ type: 'repeatMatrix', theme: 'classic' }); - expect(chart.facetCircle()).toBeInstanceOf(FacetCircle); + expect(chart.facetCircle().type).toBe('facetCircle'); expect(chart.options()).toEqual({ type: 'facetCircle', theme: 'classic' }); - expect(chart.timingKeyframe()).toBeInstanceOf(TimingKeyframe); + expect(chart.timingKeyframe().type).toBe('timingKeyframe'); expect(chart.options()).toEqual({ type: 'timingKeyframe', theme: 'classic', @@ -720,8 +677,8 @@ describe('Chart', () => { .encode('x', 'genre') .encode('y', 'sold'); await chart.render(); - expect(chart.width()).toBeUndefined(); - expect(chart.height()).toBeUndefined(); + expect(chart.attr('width')).toBeUndefined(); + expect(chart.attr('height')).toBeUndefined(); }); it('chart.options({ autoFit: true }) should bind autoFit.', async () => { diff --git a/__tests__/unit/api/composition.spec.ts b/__tests__/unit/api/composition.spec.ts index 1a2b8ec267..05e93e54b1 100644 --- a/__tests__/unit/api/composition.spec.ts +++ b/__tests__/unit/api/composition.spec.ts @@ -1,81 +1,43 @@ -import { - View, - FacetCircle, - SpaceFlex, - TimingKeyframe, - SpaceLayer, - RepeatMatrix, - FacetRect, - GeoView, - GeoPath, -} from '../../../src/api/composition'; -import { - Area, - Cell, - Image, - Interval, - Line, - Link, - Point, - Polygon, - Vector, - Box, - LineX, - LineY, - Range, - RangeX, - RangeY, - Rect, - Text, - Connector, - Sankey, - Treemap, - Pack, - ForceGraph, - Tree, - WordCloud, - Density, - Heatmap, -} from '../../../src/api/mark/mark'; +import { Chart } from '../../../src'; function expectToCreateMarks(node) { - expect(node.interval()).toBeInstanceOf(Interval); - expect(node.rect()).toBeInstanceOf(Rect); - expect(node.point()).toBeInstanceOf(Point); - expect(node.area()).toBeInstanceOf(Area); - expect(node.line()).toBeInstanceOf(Line); - expect(node.cell()).toBeInstanceOf(Cell); - expect(node.vector()).toBeInstanceOf(Vector); - expect(node.link()).toBeInstanceOf(Link); - expect(node.polygon()).toBeInstanceOf(Polygon); - expect(node.image()).toBeInstanceOf(Image); - expect(node.text()).toBeInstanceOf(Text); - expect(node.box()).toBeInstanceOf(Box); - expect(node.lineX()).toBeInstanceOf(LineX); - expect(node.lineY()).toBeInstanceOf(LineY); - expect(node.range()).toBeInstanceOf(Range); - expect(node.rangeX()).toBeInstanceOf(RangeX); - expect(node.rangeY()).toBeInstanceOf(RangeY); - expect(node.connector()).toBeInstanceOf(Connector); - expect(node.sankey()).toBeInstanceOf(Sankey); - expect(node.treemap()).toBeInstanceOf(Treemap); - expect(node.pack()).toBeInstanceOf(Pack); - expect(node.forceGraph()).toBeInstanceOf(ForceGraph); - expect(node.tree()).toBeInstanceOf(Tree); - expect(node.wordCloud()).toBeInstanceOf(WordCloud); - expect(node.density()).toBeInstanceOf(Density); - expect(node.heatmap()).toBeInstanceOf(Heatmap); + expect(node.interval().type).toBe('interval'); + expect(node.rect().type).toBe('rect'); + expect(node.point().type).toBe('point'); + expect(node.area().type).toBe('area'); + expect(node.line().type).toBe('line'); + expect(node.cell().type).toBe('cell'); + expect(node.vector().type).toBe('vector'); + expect(node.link().type).toBe('link'); + expect(node.polygon().type).toBe('polygon'); + expect(node.image().type).toBe('image'); + expect(node.text().type).toBe('text'); + expect(node.box().type).toBe('box'); + expect(node.lineX().type).toBe('lineX'); + expect(node.lineY().type).toBe('lineY'); + expect(node.range().type).toBe('range'); + expect(node.rangeX().type).toBe('rangeX'); + expect(node.rangeY().type).toBe('rangeY'); + expect(node.connector().type).toBe('connector'); + expect(node.sankey().type).toBe('sankey'); + expect(node.treemap().type).toBe('treemap'); + expect(node.pack().type).toBe('pack'); + expect(node.forceGraph().type).toBe('forceGraph'); + expect(node.tree().type).toBe('tree'); + expect(node.wordCloud().type).toBe('wordCloud'); + expect(node.density().type).toBe('density'); + expect(node.heatmap().type).toBe('heatmap'); } function expectToCreateCompositions(node) { - expect(node.view()).toBeInstanceOf(View); - expect(node.spaceLayer()).toBeInstanceOf(SpaceLayer); - expect(node.spaceFlex()).toBeInstanceOf(SpaceFlex); - expect(node.facetRect()).toBeInstanceOf(FacetRect); - expect(node.repeatMatrix()).toBeInstanceOf(RepeatMatrix); - expect(node.facetCircle()).toBeInstanceOf(FacetCircle); - expect(node.timingKeyframe()).toBeInstanceOf(TimingKeyframe); - expect(node.geoView()).toBeInstanceOf(GeoView); + expect(node.view().type).toBe('view'); + expect(node.spaceLayer().type).toBe('spaceLayer'); + expect(node.spaceFlex().type).toBe('spaceFlex'); + expect(node.facetRect().type).toBe('facetRect'); + expect(node.repeatMatrix().type).toBe('repeatMatrix'); + expect(node.facetCircle().type).toBe('facetCircle'); + expect(node.timingKeyframe().type).toBe('timingKeyframe'); + expect(node.geoView().type).toBe('geoView'); } function expectToCreateNodes(node) { @@ -84,8 +46,9 @@ function expectToCreateNodes(node) { } describe('Composition', () => { - it('View() should specify options by API', () => { - const node = new View(); + const chart = new Chart(); + it('chart.view() should specify options by API', () => { + const node = chart.view(); node .attr('paddingBottom', 10) .attr('paddingLeft', 10) @@ -128,8 +91,8 @@ describe('Composition', () => { expectToCreateMarks(node); }); - it('FacetCircle() should specify options by API', () => { - const node = new FacetCircle(); + it('chart.facetCircle() should specify options by API', () => { + const node = chart.facetCircle(); node .attr('paddingBottom', 10) .attr('paddingLeft', 10) @@ -156,8 +119,8 @@ describe('Composition', () => { expectToCreateNodes(node); }); - it('SpaceFlex() should specify options by API', () => { - const node = new SpaceFlex(); + it('chart.spaceFlex() should specify options by API', () => { + const node = chart.spaceFlex(); node .attr('direction', 'col') .data([1, 2, 3]) @@ -176,8 +139,8 @@ describe('Composition', () => { expectToCreateNodes(node); }); - it('TimingKeyframe() should specify options by API', () => { - const node = new TimingKeyframe(); + it('chart.timingKeyframe() should specify options by API', () => { + const node = chart.timingKeyframe(); node .attr('easing', 'linear') .attr('iterationCount', 10) @@ -196,8 +159,8 @@ describe('Composition', () => { expectToCreateNodes(node); }); - it('SpaceLayer() should specify options by API', () => { - const node = new SpaceLayer(); + it('chart.spaceLayer() should specify options by API', () => { + const node = chart.spaceLayer(); node.data([1, 2, 3]).attr('key', 'composition'); expect(node.type).toBe('spaceLayer'); @@ -208,8 +171,8 @@ describe('Composition', () => { expectToCreateNodes(node); }); - it('RepeatMatrix() should specify options by API', () => { - const node = new RepeatMatrix(); + it('chart.repeatMatrix() should specify options by API', () => { + const node = chart.repeatMatrix(); node .data([1, 2, 3]) .attr('key', 'composition') @@ -236,8 +199,8 @@ describe('Composition', () => { expectToCreateNodes(node); }); - it('Rect() should specify options by API', () => { - const node = new FacetRect(); + it('chart.facetRect() should specify options by API', () => { + const node = chart.facetRect(); node .data([1, 2, 3]) .attr('key', 'composition') @@ -272,8 +235,8 @@ describe('Composition', () => { expectToCreateNodes(node); }); - it('GeoView() should specify options by API', () => { - const node = new GeoView(); + it('chart.geoView() should specify options by API', () => { + const node = chart.geoView(); node .attr('paddingBottom', 10) .attr('paddingLeft', 10) @@ -314,8 +277,8 @@ describe('Composition', () => { expectToCreateMarks(node); }); - it('GeoPath() should specify options by API', () => { - const node = new GeoPath(); + it('chart.geoPath() should specify options by API', () => { + const node = chart.geoPath(); node .attr('paddingBottom', 10) .attr('paddingLeft', 10) diff --git a/__tests__/unit/api/mark.spec.ts b/__tests__/unit/api/mark.spec.ts index e179e062c0..9f80bc130a 100644 --- a/__tests__/unit/api/mark.spec.ts +++ b/__tests__/unit/api/mark.spec.ts @@ -1,56 +1,7 @@ -import { Chart } from '../../../src'; +import { Chart, MarkNode } from '../../../src'; import { G2Mark } from '../../../src/runtime'; -import { - Area, - Interval, - Point, - Cell, - Vector, - Link, - Polygon, - Image, - Text, - Box, - Connector, - LineX, - LineY, - Range, - RangeX, - RangeY, - Sankey, - Treemap, - Boxplot, - Shape, - Pack, - ForceGraph, - Tree, - WordCloud, - Gauge, - AxisX, - AxisY, - Legends, -} from '../../../src/api/mark/mark'; - -type Mark = - | Area - | Interval - | Point - | Cell - | Vector - | Link - | Polygon - | Image - | Text - | Box - | Connector - | LineX - | LineY - | Range - | RangeX - | RangeY - | Shape; - -function setOptions(node: Mark) { + +function setOptions(node: MarkNode) { return node .data([1, 2, 3]) .encode('x', 'name') @@ -90,16 +41,15 @@ function setOptions(node: Mark) { .state('inactive', { fill: 'blue' }); } -// @todo Fix type errors. -function setCompositeOptions(node) { +function setCompositeOptions(node: MarkNode) { return node.call(setOptions); } -function setOptions2(node: Mark) { +function setOptions2(node: MarkNode) { return node.tooltip(false); } -function setLayoutOptions(node) { +function setLayoutOptions(node: MarkNode) { return node.call(setOptions).layout({ a: 10, b: 8, @@ -164,12 +114,10 @@ function getLayoutOptions() { }; } -function setAxisOptions( - node: AxisX | AxisY | Legends, -): AxisX | AxisY | Legends { +function setAxisOptions(node: MarkNode): MarkNode { return node .scale('x', { type: 'linear' }) - .transform({ type: 'hide' }) + .transform({ type: 'stackY' }) .style('gridFill', 'red') .state('active', { gridFill: 'red' }) .attr('labelFormatter', '~s'); @@ -178,7 +126,7 @@ function setAxisOptions( function getAxisOptions() { return { scale: { x: { type: 'linear' } }, - transform: [{ type: 'hide' }], + transform: [{ type: 'stackY' }], style: { gridFill: 'red' }, state: { active: { gridFill: 'red' } }, labelFormatter: '~s', @@ -234,187 +182,188 @@ describe('mark.get[Instance]()', () => { }); describe('mark.[node]()', () => { - it('Interval() should specify options by API', () => { - const node = new Interval(); + const chart = new Chart(); + it('chart.interval() should specify options by API', () => { + const node = chart.interval(); expect(node.type).toBe('interval'); expect(setOptions(node).value).toEqual(getOptions()); expect(setOptions2(node).value).toEqual(getOptions2()); }); - it('Point() should specify options by API', () => { - const node = new Point(); + it('chart.point() should specify options by API', () => { + const node = chart.point(); expect(node.type).toBe('point'); expect(setOptions(node).value).toEqual(getOptions()); expect(setOptions2(node).value).toEqual(getOptions2()); }); - it('Area() should specify options by API', () => { - const node = new Area(); + it('chart.area() should specify options by API', () => { + const node = chart.area(); expect(node.type).toBe('area'); expect(setOptions(node).value).toEqual(getOptions()); expect(setOptions2(node).value).toEqual(getOptions2()); }); - it('Cell() should specify options by API', () => { - const node = new Cell(); + it('chart.cell() should specify options by API', () => { + const node = chart.cell(); expect(node.type).toBe('cell'); expect(setOptions(node).value).toEqual(getOptions()); expect(setOptions2(node).value).toEqual(getOptions2()); }); - it('Vector() should specify options by API', () => { - const node = new Vector(); + it('chart.vector() should specify options by API', () => { + const node = chart.vector(); expect(node.type).toBe('vector'); expect(setOptions(node).value).toEqual(getOptions()); expect(setOptions2(node).value).toEqual(getOptions2()); }); - it('Link() should specify options by API', () => { - const node = new Link(); + it('chart.link() should specify options by API', () => { + const node = chart.link(); expect(node.type).toBe('link'); expect(setOptions(node).value).toEqual(getOptions()); expect(setOptions2(node).value).toEqual(getOptions2()); }); - it('Polygon() should specify options by API', () => { - const node = new Polygon(); + it('chart.polygon() should specify options by API', () => { + const node = chart.polygon(); expect(node.type).toBe('polygon'); expect(setOptions(node).value).toEqual(getOptions()); expect(setOptions2(node).value).toEqual(getOptions2()); }); - it('Image() should specify options by API', () => { - const node = new Image(); + it('chart.image() should specify options by API', () => { + const node = chart.image(); expect(node.type).toBe('image'); expect(setOptions(node).value).toEqual(getOptions()); expect(setOptions2(node).value).toEqual(getOptions2()); }); - it('Text() should specify options by API', () => { - const node = new Text(); + it('chart.text() should specify options by API', () => { + const node = chart.text(); expect(node.type).toBe('text'); expect(setOptions(node).value).toEqual(getOptions()); expect(setOptions2(node).value).toEqual(getOptions2()); }); - it('Box() should specify options by API', () => { - const node = new Box(); + it('chart.box() should specify options by API', () => { + const node = chart.box(); expect(node.type).toBe('box'); expect(setOptions(node).value).toEqual(getOptions()); expect(setOptions2(node).value).toEqual(getOptions2()); }); - it('Connector() should specify options by API', () => { - const node = new Connector(); + it('chart.connector() should specify options by API', () => { + const node = chart.connector(); expect(node.type).toBe('connector'); expect(setOptions(node).value).toEqual(getOptions()); expect(setOptions2(node).value).toEqual(getOptions2()); }); - it('Range() should specify options by API', () => { - const node = new Range(); + it('chart.range() should specify options by API', () => { + const node = chart.range(); expect(node.type).toBe('range'); expect(setOptions(node).value).toEqual(getOptions()); expect(setOptions2(node).value).toEqual(getOptions2()); }); - it('RangeX() should specify options by API', () => { - const node = new RangeX(); + it('chart.rangeX() should specify options by API', () => { + const node = chart.rangeX(); expect(node.type).toBe('rangeX'); expect(setOptions(node).value).toEqual(getOptions()); expect(setOptions2(node).value).toEqual(getOptions2()); }); - it('RangeY() should specify options by API', () => { - const node = new RangeY(); + it('chart.rangeY() should specify options by API', () => { + const node = chart.rangeY(); expect(node.type).toBe('rangeY'); expect(setOptions(node).value).toEqual(getOptions()); expect(setOptions2(node).value).toEqual(getOptions2()); }); - it('LineX() should specify options by API', () => { - const node = new LineX(); + it('chart.lineX() should specify options by API', () => { + const node = chart.lineX(); expect(node.type).toBe('lineX'); expect(setOptions(node).value).toEqual(getOptions()); expect(setOptions2(node).value).toEqual(getOptions2()); }); - it('LineY() should specify options by API', () => { - const node = new LineY(); + it('chart.lineY() should specify options by API', () => { + const node = chart.lineY(); expect(node.type).toBe('lineY'); expect(setOptions(node).value).toEqual(getOptions()); expect(setOptions2(node).value).toEqual(getOptions2()); }); - it('Shape() should specify options by API', () => { - const node = new Shape(); + it('chart.shape() should specify options by API', () => { + const node = chart.shape(); expect(node.type).toBe('shape'); expect(setOptions(node).value).toEqual(getOptions()); expect(setOptions2(node).value).toEqual(getOptions2()); }); - it('Boxplot() should specify options by API', () => { - const node = new Boxplot(); + it('chart.boxplot() should specify options by API', () => { + const node = chart.boxplot(); expect(node.type).toBe('boxplot'); expect(setCompositeOptions(node).value).toEqual(getOptions()); }); - it('Sankey() should specify options by API', () => { - const node = new Sankey(); + it('chart.sankey() should specify options by API', () => { + const node = chart.sankey(); expect(node.type).toBe('sankey'); expect(setLayoutOptions(node).value).toEqual(getLayoutOptions()); }); - it('Treemap() should specify options by API', () => { - const node = new Treemap(); + it('treemap() should specify options by API', () => { + const node = chart.treemap(); expect(node.type).toBe('treemap'); expect(setLayoutOptions(node).value).toEqual(getLayoutOptions()); }); - it('Pack() should specify options by API', () => { - const node = new Pack(); + it('chart.pack() should specify options by API', () => { + const node = chart.pack(); expect(node.type).toBe('pack'); expect(setLayoutOptions(node).value).toEqual(getLayoutOptions()); }); - it('ForceGraph() should specify options by API', () => { - const node = new ForceGraph(); + it('chart.forceGraph() should specify options by API', () => { + const node = chart.forceGraph(); expect(node.type).toBe('forceGraph'); expect(setLayoutOptions(node).value).toEqual(getLayoutOptions()); }); - it('Tree() should specify options by API', () => { - const node = new Tree(); + it('chart.tree() should specify options by API', () => { + const node = chart.tree(); expect(node.type).toBe('tree'); expect(setLayoutOptions(node).value).toEqual(getLayoutOptions()); }); - it('WordCloud() should specify options by API', () => { - const node = new WordCloud(); + it('chart.wordCloud() should specify options by API', () => { + const node = chart.wordCloud(); expect(node.type).toBe('wordCloud'); expect(setLayoutOptions(node).value).toEqual(getLayoutOptions()); }); - it('Gauge() should specify options by API', () => { - const node = new Gauge(); + it('chart.gauge() should specify options by API', () => { + const node = chart.gauge(); expect(node.type).toBe('gauge'); expect(setCompositeOptions(node).value).toEqual(getOptions()); }); - it('AxisX() should specify options by API', () => { - const node = new AxisX(); + it('chart.axisX() should specify options by API', () => { + const node = chart.axisX(); expect(node.type).toBe('axisX'); expect(setAxisOptions(node).value).toEqual(getAxisOptions()); }); - it('AxisY() should specify options by API', () => { - const node = new AxisY(); + it('chart.axisY() should specify options by API', () => { + const node = chart.axisY(); expect(node.type).toBe('axisY'); expect(setAxisOptions(node).value).toEqual(getAxisOptions()); }); - it('Legends() should specify options by API', () => { - const node = new Legends(); + it('chart.legends() should specify options by API', () => { + const node = chart.legends(); expect(node.type).toBe('legends'); expect(setAxisOptions(node).value).toEqual(getAxisOptions()); }); diff --git a/__tests__/unit/api/options.spec.ts b/__tests__/unit/api/options.spec.ts index dce703c99d..06c3a3105b 100644 --- a/__tests__/unit/api/options.spec.ts +++ b/__tests__/unit/api/options.spec.ts @@ -1,5 +1,4 @@ import { Chart } from '../../../src'; -import { Point } from '../../../src/api/mark/mark'; describe('chart api and options', () => { it('chart.options({...}) should create node instance from spec.', () => { @@ -46,8 +45,8 @@ describe('chart api and options', () => { marginRight: 10, marginTop: 10, marginBottom: 10, - autoFit: 10, - theme: 10, + autoFit: true, + theme: 'light', }); expect(chart.options()).toEqual({ @@ -69,8 +68,8 @@ describe('chart api and options', () => { marginRight: 10, marginTop: 10, marginBottom: 10, - autoFit: 10, - theme: 10, + autoFit: true, + theme: 'light', children: [{ type: 'interval' }], }); }); @@ -100,7 +99,7 @@ describe('chart api and options', () => { chart.options(options); expect(chart.options()).toEqual(options); - expect(chart.getNodeByType('point')).toBeInstanceOf(Point); + expect(chart.getNodeByType('point').type).toBe('point'); }); it('chart.options({...} should update mark.', () => { diff --git a/__tests__/unit/api/props.spec.ts b/__tests__/unit/api/props.spec.ts index 991faca017..9e35be4679 100644 --- a/__tests__/unit/api/props.spec.ts +++ b/__tests__/unit/api/props.spec.ts @@ -1,9 +1,9 @@ -import { defineProps } from '../../../src/api/props'; +import { defineProps } from '../../../src/api/define'; import { Node } from '../../../src/api/node'; describe('defineProps', () => { it('defineProps([...]) should define value prop', () => { - const N = defineProps([{ type: 'value', name: 'a' }])(Node); + const N = defineProps({ a: { type: 'value' } })(Node); const n = new N({ a: 1 }); expect(n.a()).toBe(1); const n1 = n.a(2); @@ -13,14 +13,14 @@ describe('defineProps', () => { }); it('defineProps([...]) should define keyed value prop', () => { - const N = defineProps([{ type: 'value', name: 'a', key: 'b' }])(Node); + const N = defineProps({ a: { type: 'value', key: 'b' } })(Node); const n = new N({}); n.a(2); expect(n.value).toEqual({ b: 2 }); }); it('defineProps([...]) should define array prop', () => { - const N = defineProps([{ type: 'array', name: 'a' }])(Node); + const N = defineProps({ a: { type: 'array' } })(Node); const n = new N({ a: [1] }); expect(n.a()).toEqual([1]); const n1 = n.a(2); @@ -40,14 +40,14 @@ describe('defineProps', () => { }); it('defineProps([...]) should define keyed array prop', () => { - const N = defineProps([{ type: 'array', name: 'a', key: 'b' }])(Node); + const N = defineProps({ a: { type: 'array', key: 'b' } })(Node); const n = new N({}); n.a(2); expect(n.value).toEqual({ b: [2] }); }); it('defineProps([...]) should define object prop', () => { - const N = defineProps([{ type: 'object', name: 'a' }])(Node); + const N = defineProps({ a: { type: 'object' } })(Node); const n = new N({ a: { b: 1 } }); expect(n.a()).toEqual({ b: 1 }); const n1 = n.a('b', 2); @@ -63,7 +63,7 @@ describe('defineProps', () => { }); it('defineProps([...]) should define boolean object prop', () => { - const N = defineProps([{ type: 'object', name: 'a' }])(Node); + const N = defineProps({ a: { type: 'object' } })(Node); const n = new N(); n.a('b'); expect(n.a()).toEqual({ b: true }); @@ -72,14 +72,14 @@ describe('defineProps', () => { }); it('defineProps([...]) should define keyed object prop', () => { - const N = defineProps([{ type: 'object', name: 'a', key: 'b' }])(Node); + const N = defineProps({ a: { type: 'object', key: 'b' } })(Node); const n = new N({}); n.a('a', 1); expect(n.value).toEqual({ b: { a: 1 } }); }); it('defineProps([...]) should define node prop', () => { - const N = defineProps([{ type: 'node', ctor: Node, name: 'a' }])(Node); + const N = defineProps({ a: { type: 'node', ctor: Node } })(Node); const n = new N(); const n1 = n.a(); expect(n1.parentNode).toBe(n); @@ -88,7 +88,7 @@ describe('defineProps', () => { }); it('defineProps([...]) should define container prop', () => { - const N = defineProps([{ type: 'container', ctor: Node, name: 'a' }])(Node); + const N = defineProps({ a: { type: 'container', ctor: Node } })(Node); const n = new N(); const n1 = n.a(); expect(n1.parentNode).toBe(n); @@ -98,7 +98,7 @@ describe('defineProps', () => { }); it('definedProps([...]) should define mix prop', () => { - const N = defineProps([{ type: 'mix', name: 'mix' }])(Node); + const N = defineProps({ mix: { type: 'mix' } })(Node); const n = new N(); n.mix('a'); n.mix('b'); diff --git a/__tests__/utils/renderToMountedElement.ts b/__tests__/utils/renderToMountedElement.ts index 0b7a1839ee..42b944d7b1 100644 --- a/__tests__/utils/renderToMountedElement.ts +++ b/__tests__/utils/renderToMountedElement.ts @@ -3,7 +3,7 @@ import { renderToMountedElement as r } from '../../src'; export function renderToMountedElement( options, - { canvas }, + { canvas, ...rest }, resolve = () => {}, ) { canvas.ready.then(() => { @@ -17,7 +17,7 @@ export function renderToMountedElement( ); const group = new Group({}); canvas.appendChild(group); - r(options, { group }, resolve); + r(options, { group, ...rest }, resolve); }); return canvas.getConfig().container; } diff --git a/src/api/chart.ts b/src/api/chart.ts index 463ae47423..29879a3e80 100644 --- a/src/api/chart.ts +++ b/src/api/chart.ts @@ -1,344 +1,9 @@ -import { IRenderer, RendererPlugin, Canvas as GCanvas } from '@antv/g'; -import { Renderer as CanvasRenderer } from '@antv/g-canvas'; -import { Plugin as DragAndDropPlugin } from '@antv/g-plugin-dragndrop'; -import { debounce } from '@antv/util'; -import EventEmitter from '@antv/event-emitter'; -import { G2Context, render, destroy } from '../runtime'; -import { ViewComposition } from '../spec'; -import { ChartEvent } from '../utils/event'; -import { G2ViewTree } from '../runtime/types/options'; -import { - defineProps, - NodePropertyDescriptor, - nodeProps, - containerProps, -} from './props'; -import { - ValueAttribute, - Concrete, - ArrayAttribute, - ObjectAttribute, -} from './types'; -import { mark, Mark } from './mark'; -import { composition, Composition, View } from './composition'; +import { G2Spec } from '../spec'; +import { extend } from './extend'; import { library } from './library'; -import { - normalizeContainer, - removeContainer, - sizeOf, - optionsOf, - updateRoot, - createEmptyPromise, -} from './utils'; +import { Runtime } from './runtime'; -export const G2_CHART_KEY = 'G2_CHART_KEY'; +// Use a empty interface to mark Chart both a value and a class type. +export interface Chart {} -export type ChartOptions = ViewComposition & { - container?: string | HTMLElement; - canvas?: GCanvas; - width?: number; - height?: number; - autoFit?: boolean; - renderer?: IRenderer; - plugins?: RendererPlugin[]; - theme?: string; -}; - -type ChartProps = Concrete; - -export interface Chart extends Composition, Mark { - data: ValueAttribute; - width: ValueAttribute; - height: ValueAttribute; - coordinate: ValueAttribute; - interaction: ObjectAttribute; - key: ValueAttribute; - transform: ArrayAttribute; - theme: ObjectAttribute; - title: ValueAttribute; - scale: ObjectAttribute; - axis: ObjectAttribute; - legend: ObjectAttribute; - style: ObjectAttribute; - labelTransform: ArrayAttribute; -} - -export const props: NodePropertyDescriptor[] = [ - { name: 'data', type: 'value' }, - { name: 'width', type: 'value' }, - { name: 'height', type: 'value' }, - { name: 'coordinate', type: 'value' }, - { name: 'interaction', type: 'object' }, - { name: 'theme', type: 'object' }, - { name: 'title', type: 'value' }, - { name: 'transform', type: 'array' }, - { name: 'scale', type: 'object' }, - { name: 'axis', type: 'object' }, - { name: 'legend', type: 'object' }, - { name: 'style', type: 'object' }, - { name: 'labelTransform', type: 'array' }, - ...nodeProps(mark), - ...containerProps(composition), -]; - -@defineProps(props) -export class Chart extends View { - private _container: HTMLElement; - private _context: G2Context; - private _emitter: EventEmitter; - private _width: number; - private _height: number; - private _renderer: IRenderer; - private _plugins: RendererPlugin[]; - // Identifies whether bindAutoFit. - private _hasBindAutoFit = false; - private _rendering = false; - private _trailing = false; - private _trailingResolve = null; - private _trailingReject = null; - private _previousDefinedType = null; - - constructor(options: ChartOptions) { - const { container, canvas, renderer, plugins, ...rest } = options || {}; - super(rest, 'view'); - this._renderer = renderer || new CanvasRenderer(); - this._plugins = plugins || []; - this._container = normalizeContainer(container); - this._emitter = new EventEmitter(); - this._context = { library, emitter: this._emitter, canvas }; - } - - render(): Promise { - if (this._rendering) return this._addToTrailing(); - if (!this._context.canvas) this._createCanvas(); - this._bindAutoFit(); - this._rendering = true; - const finished = new Promise((resolve, reject) => - render( - this._computedOptions(), - this._context, - this._createResolve(resolve), - this._createReject(reject), - ), - ); - const [finished1, resolve, reject] = createEmptyPromise(); - finished - .then(resolve) - .catch(reject) - .then(() => this._renderTrailing()); - return finished1; - } - - /** - * @overload - * @returns {G2ViewTree} - */ - options(): G2ViewTree; - /** - * @overload - * @param {G2ViewTree} options - * @returns {Chart} - */ - options(options: G2ViewTree): Chart; - /** - * @overload - * @param {G2ViewTree} [options] - * @returns {Chart|G2ViewTree} - */ - options(options?: G2ViewTree): Chart | G2ViewTree { - if (arguments.length === 0) return optionsOf(this); - const { type } = options; - if (type) this._previousDefinedType = type; - updateRoot(this, options, this._previousDefinedType); - return this; - } - - getContainer(): HTMLElement { - return this._container; - } - - getContext(): G2Context { - return this._context; - } - - on(event: string, callback: (...args: any[]) => any, once?: boolean): this { - this._emitter.on(event, callback, once); - return this; - } - - once(event: string, callback: (...args: any[]) => any): this { - this._emitter.once(event, callback); - return this; - } - - emit(event: string, ...args: any[]): this { - this._emitter.emit(event, ...args); - return this; - } - - off(event?: string, callback?: (...args: any[]) => any) { - this._emitter.off(event, callback); - return this; - } - - clear() { - const options = this.options(); - this.emit(ChartEvent.BEFORE_CLEAR); - this._reset(); - destroy(options, this._context, false); - this.emit(ChartEvent.AFTER_CLEAR); - } - - destroy() { - const options = this.options(); - this.emit(ChartEvent.BEFORE_DESTROY); - this._unbindAutoFit(); - this._reset(); - destroy(options, this._context, true); - removeContainer(this._container); - this.emit(ChartEvent.AFTER_DESTROY); - } - - forceFit() { - // Don't fit if size do not change. - this.options['autoFit'] = true; - const { width, height } = sizeOf(this.options(), this._container); - if (width === this._width && height === this._height) { - return Promise.resolve(this); - } - - // Don't call changeSize to prevent update width and height of options. - this.emit(ChartEvent.BEFORE_CHANGE_SIZE); - const finished = this.render(); - finished.then(() => { - this.emit(ChartEvent.AFTER_CHANGE_SIZE); - }); - return finished; - } - - changeSize(width: number, height: number): Promise { - if (width === this._width && height === this._height) { - return Promise.resolve(this); - } - this.emit(ChartEvent.BEFORE_CHANGE_SIZE); - this.width(width); - this.height(height); - const finished = this.render(); - finished.then(() => { - this.emit(ChartEvent.AFTER_CHANGE_SIZE); - }); - return finished; - } - - private _reset() { - const KEYS = ['theme', 'type', 'width', 'height', 'autoFit']; - this.type = 'view'; - this.value = Object.fromEntries( - Object.entries(this.value).filter( - ([key]) => - key.startsWith('margin') || - key.startsWith('padding') || - key.startsWith('inset') || - KEYS.includes(key), - ), - ); - 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 { - // Resolve previous promise, and give up this task. - this._trailingResolve?.(this); - - // Create new task. - this._trailing = true; - const promise = new Promise((resolve, reject) => { - this._trailingResolve = resolve; - this._trailingReject = reject; - }); - - return promise; - } - - private _onResize = debounce(() => { - this.forceFit(); - }, 300); - - private _bindAutoFit() { - const options = this.options(); - const { autoFit } = options; - - if (this._hasBindAutoFit) { - // If it was bind before, unbind it now. - if (!autoFit) this._unbindAutoFit(); - return; - } - - if (autoFit) { - this._hasBindAutoFit = true; - window.addEventListener('resize', this._onResize); - } - } - - private _unbindAutoFit() { - if (this._hasBindAutoFit) { - this._hasBindAutoFit = false; - window.removeEventListener('resize', this._onResize); - } - } -} +export const Chart = extend(Runtime, library); diff --git a/src/api/composition/base.ts b/src/api/composition.ts similarity index 72% rename from src/api/composition/base.ts rename to src/api/composition.ts index 08339d7284..705b201111 100644 --- a/src/api/composition/base.ts +++ b/src/api/composition.ts @@ -1,14 +1,22 @@ import { Coordinate } from '@antv/coord'; import { DisplayObject } from '@antv/g'; -import { Scale, G2Theme, G2ViewDescriptor } from '../../runtime'; -import { hide, show } from '../../utils/style'; -import { Node } from '../node'; +import { Scale, G2Theme, G2ViewDescriptor } from '../runtime'; +import { hide, show } from '../utils/style'; +import { Composition as Spec } from '../spec'; +import { Node } from './node'; +import { defineProps } from './define'; +import { compositionProps } from './props'; +import { PropsOf } from './types'; -export class CompositionNode< - Value extends Record = Record, - ParentValue extends Record = Record, - ChildValue extends Record = Record, -> extends Node { +export interface CompositionNode + extends PropsOf< + typeof compositionProps, + any, // todo Remove this when update types of Spec. + CompositionNode & T + > {} + +@defineProps(compositionProps) +export class CompositionNode extends Node { protected _key: string; /** diff --git a/src/api/composition/facetCircle.ts b/src/api/composition/facetCircle.ts deleted file mode 100644 index 05fd5db18f..0000000000 --- a/src/api/composition/facetCircle.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { FacetCircleComposition } from '../../spec'; -import { defineProps, nodeProps } from '../props'; -import { - ValueAttribute, - Concrete, - ArrayAttribute, - ObjectAttribute, -} from '../types'; -import { mark, Mark } from '../mark'; -import { Composition, CompositionNode } from './index'; - -type FacetCircleSpec = Concrete; - -export interface FacetCircle extends Composition, Mark { - data: ValueAttribute; - transform: ArrayAttribute; - encode: ObjectAttribute; - scale: ObjectAttribute; - legend: ObjectAttribute; - axis: ObjectAttribute; -} - -@defineProps([ - { type: 'value', name: 'data' }, - { type: 'array', name: 'transform' }, - { type: 'object', name: 'scale' }, - { type: 'object', name: 'encode' }, - { type: 'object', name: 'legend' }, - { type: 'object', name: 'axis' }, - ...nodeProps(mark), -]) -export class FacetCircle extends CompositionNode { - constructor() { - super({}, 'facetCircle'); - } -} diff --git a/src/api/composition/facetRect.ts b/src/api/composition/facetRect.ts deleted file mode 100644 index 3a90b01969..0000000000 --- a/src/api/composition/facetRect.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { FacetRectComposition } from '../../spec'; -import { defineProps, nodeProps } from '../props'; -import { - ValueAttribute, - Concrete, - ArrayAttribute, - ObjectAttribute, -} from '../types'; -import { mark, Mark } from '../mark'; -import { Composition, CompositionNode } from './index'; - -type FacetRectSpec = Concrete; - -export interface FacetRect extends Composition, Mark { - data: ValueAttribute; - transform: ArrayAttribute; - encode: ObjectAttribute; - scale: ObjectAttribute; - legend: ObjectAttribute; - axis: ObjectAttribute; -} - -@defineProps([ - { type: 'value', name: 'data' }, - { type: 'array', name: 'transform' }, - { type: 'object', name: 'scale' }, - { type: 'object', name: 'encode' }, - { type: 'object', name: 'legend' }, - { type: 'object', name: 'axis' }, - ...nodeProps(mark), -]) -export class FacetRect extends CompositionNode { - constructor() { - super({}, 'facetRect'); - } -} diff --git a/src/api/composition/geoPath.ts b/src/api/composition/geoPath.ts deleted file mode 100644 index d149a86f53..0000000000 --- a/src/api/composition/geoPath.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { GeoPathComposition } from '../../spec'; -import { defineProps, nodeProps } from '../props'; -import { ValueAttribute, ObjectAttribute, Concrete } from '../types'; -import { mark, Mark } from '../mark'; -import { Composition, CompositionNode } from './index'; - -type GeoPathSpec = Concrete; - -export interface GeoPath extends Mark, Composition { - data: ValueAttribute; - coordinate: ValueAttribute; - interaction: ObjectAttribute; - style: ObjectAttribute; - theme: ObjectAttribute; - scale: ObjectAttribute; - encode: ObjectAttribute; - legend: ObjectAttribute; - state: ObjectAttribute; -} - -@defineProps([ - { type: 'value', name: 'data' }, - { type: 'value', name: 'coordinate' }, - { type: 'object', name: 'interaction' }, - { type: 'object', name: 'theme' }, - { type: 'object', name: 'style' }, - { type: 'object', name: 'scale' }, - { type: 'object', name: 'encode' }, - { type: 'object', name: 'legend' }, - { type: 'object', name: 'state' }, - ...nodeProps(mark), -]) -export class GeoPath extends CompositionNode { - constructor() { - super({}, 'geoPath'); - } -} diff --git a/src/api/composition/geoView.ts b/src/api/composition/geoView.ts deleted file mode 100644 index a7f5b02b12..0000000000 --- a/src/api/composition/geoView.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { GeoViewComposition } from '../../spec'; -import { defineProps, nodeProps } from '../props'; -import { - ValueAttribute, - ObjectAttribute, - ArrayAttribute, - Concrete, -} from '../types'; -import { mark, Mark } from '../mark'; -import { Composition, CompositionNode } from './index'; - -type GeoViewSpec = Concrete; - -export interface GeoView extends Mark, Composition { - data: ValueAttribute; - key: ValueAttribute; - coordinate: ArrayAttribute; - interaction: ObjectAttribute; - style: ObjectAttribute; - theme: ObjectAttribute; - scale: ObjectAttribute; - legend: ObjectAttribute; -} - -@defineProps([ - { type: 'value', name: 'data' }, - { type: 'value', name: 'coordinate' }, - { type: 'object', name: 'interaction' }, - { type: 'object', name: 'theme' }, - { type: 'object', name: 'style' }, - { type: 'object', name: 'scale' }, - { type: 'object', name: 'legend' }, - ...nodeProps(mark), -]) -export class GeoView extends CompositionNode { - constructor() { - super({}, 'geoView'); - } -} diff --git a/src/api/composition/index.ts b/src/api/composition/index.ts deleted file mode 100644 index 5d04c4dc8d..0000000000 --- a/src/api/composition/index.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { defineProps, nodeProps } from '../props'; -import { CompositionNode } from './base'; -import { View } from './view'; -import { SpaceLayer } from './spaceLayer'; -import { SpaceFlex } from './spaceFlex'; -import { FacetRect } from './facetRect'; -import { FacetCircle } from './facetCircle'; -import { RepeatMatrix } from './repeatMatrix'; -import { TimingKeyframe } from './timingKeyframe'; -import { GeoView } from './geoView'; -import { GeoPath } from './geoPath'; - -export const composition = { - view: View, - spaceLayer: SpaceLayer, - spaceFlex: SpaceFlex, - facetRect: FacetRect, - facetCircle: FacetCircle, - repeatMatrix: RepeatMatrix, - timingKeyframe: TimingKeyframe, - geoView: GeoView, - geoPath: GeoPath, -}; - -export interface Composition { - view(): View; - spaceLayer(): SpaceLayer; - spaceFlex(): SpaceFlex; - facetRect(): FacetRect; - facetCircle(): FacetCircle; - repeatMatrix(): RepeatMatrix; - timingKeyframe(): TimingKeyframe; - geoView(): GeoView; - geoPath(): GeoPath; -} - -export { - CompositionNode, - View, - SpaceLayer, - SpaceFlex, - FacetRect, - FacetCircle, - RepeatMatrix, - TimingKeyframe, - GeoView, - GeoPath, -}; - -/** - * Define composition node api for composition node dynamically, - * which can avoid circular dependency. - * @todo Remove view as composition. - */ -for (const Ctor of Object.values(composition)) { - defineProps(nodeProps(composition))(Ctor); -} diff --git a/src/api/composition/repeatMatrix.ts b/src/api/composition/repeatMatrix.ts deleted file mode 100644 index 893a402003..0000000000 --- a/src/api/composition/repeatMatrix.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { RepeatMatrixComposition } from '../../spec'; -import { defineProps, nodeProps } from '../props'; -import { - ValueAttribute, - Concrete, - ArrayAttribute, - ObjectAttribute, -} from '../types'; -import { mark, Mark } from '../mark'; -import { Composition, CompositionNode } from './index'; - -type RepeatMatrixSpec = Concrete; - -export interface RepeatMatrix extends Composition, Mark { - data: ValueAttribute; - transform: ArrayAttribute; - encode: ObjectAttribute; - scale: ObjectAttribute; - legend: ObjectAttribute; - axis: ObjectAttribute; -} - -@defineProps([ - { type: 'value', name: 'data' }, - { type: 'array', name: 'transform' }, - { type: 'object', name: 'scale' }, - { type: 'object', name: 'encode' }, - { type: 'object', name: 'encode' }, - { type: 'object', name: 'legend' }, - ...nodeProps(mark), -]) -export class RepeatMatrix extends CompositionNode { - constructor() { - super({}, 'repeatMatrix'); - } -} diff --git a/src/api/composition/spaceFlex.ts b/src/api/composition/spaceFlex.ts deleted file mode 100644 index 6c8591832d..0000000000 --- a/src/api/composition/spaceFlex.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { SpaceFlexComposition } from '../../spec'; -import { defineProps, nodeProps } from '../props'; -import { ValueAttribute, Concrete } from '../types'; -import { mark, Mark } from '../mark'; -import { Composition, CompositionNode } from './index'; - -type SpaceFlexSpec = Concrete; - -export interface SpaceFlex extends Composition, Mark { - data: ValueAttribute; -} - -@defineProps([{ type: 'value', name: 'data' }, ...nodeProps(mark)]) -export class SpaceFlex extends CompositionNode { - constructor() { - super({}, 'spaceFlex'); - } -} diff --git a/src/api/composition/spaceLayer.ts b/src/api/composition/spaceLayer.ts deleted file mode 100644 index 2b88ab8538..0000000000 --- a/src/api/composition/spaceLayer.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { SpaceLayerComposition } from '../../spec'; -import { defineProps, nodeProps } from '../props'; -import { ValueAttribute, Concrete } from '../types'; -import { mark, Mark } from '../mark'; -import { Composition, CompositionNode } from './index'; - -type SpaceLayerSpec = Concrete; - -export interface SpaceLayer extends Composition, Mark { - data: ValueAttribute; -} - -@defineProps([{ type: 'value', name: 'data' }, ...nodeProps(mark)]) -export class SpaceLayer extends CompositionNode { - constructor() { - super({}, 'spaceLayer'); - } -} diff --git a/src/api/composition/timingKeyframe.ts b/src/api/composition/timingKeyframe.ts deleted file mode 100644 index 125326b426..0000000000 --- a/src/api/composition/timingKeyframe.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { TimingKeyframeComposition } from '../../spec'; -import { defineProps, nodeProps } from '../props'; -import { Concrete } from '../types'; -import { mark, Mark } from '../mark'; -import { Composition, CompositionNode } from './index'; - -type TimingKeyframeSpec = Concrete; - -export interface TimingKeyframe extends Composition, Mark {} - -@defineProps([...nodeProps(mark)]) -export class TimingKeyframe extends CompositionNode { - constructor() { - super({}, 'timingKeyframe'); - } -} diff --git a/src/api/composition/view.ts b/src/api/composition/view.ts deleted file mode 100644 index 82e854e492..0000000000 --- a/src/api/composition/view.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ViewComposition } from '../../spec'; -import { defineProps, nodeProps } from '../props'; -import { - ValueAttribute, - ObjectAttribute, - ArrayAttribute, - Concrete, -} from '../types'; -import { mark, Mark } from '../mark'; -import { CompositionNode } from './base'; - -type ViewSpec = Concrete; - -export interface View extends Mark { - data: ValueAttribute; - coordinate: ArrayAttribute; - interaction: ObjectAttribute; - theme: ObjectAttribute; - style: ObjectAttribute; - scale: ObjectAttribute; - axis: ObjectAttribute; - legend: ObjectAttribute; -} - -@defineProps([ - { type: 'value', name: 'data' }, - { type: 'value', name: 'coordinate' }, - { type: 'object', name: 'interaction' }, - { type: 'object', name: 'theme' }, - { type: 'object', name: 'style' }, - { type: 'object', name: 'scale' }, - { type: 'object', name: 'axis' }, - { type: 'object', name: 'legend' }, - ...nodeProps(mark), -]) -export class View< - ViewProps extends ViewComposition = ViewComposition, -> extends CompositionNode { - constructor(options = {}, type = 'view') { - super(options, type); - } -} diff --git a/src/api/define.ts b/src/api/define.ts new file mode 100644 index 0000000000..ef61861221 --- /dev/null +++ b/src/api/define.ts @@ -0,0 +1,128 @@ +import { isStrictObject } from '../utils/helper'; +import { Node } from './node'; + +export type NodePropertyDescriptor = { + type: 'object' | 'value' | 'array' | 'node' | 'container' | 'mix'; + key?: string; + ctor?: new (...args: any[]) => any; +}; + +type NodeClass = new (props: any, type?: string) => Node; + +function defineValueProp( + Node: NodeClass, + name: string, + { key = name }: NodePropertyDescriptor, +) { + Node.prototype[name] = function (value) { + if (arguments.length === 0) return this.attr(key); + return this.attr(key, value); + }; +} + +function defineArrayProp( + Node: NodeClass, + name: string, + { key = name }: NodePropertyDescriptor, +) { + Node.prototype[name] = function (value) { + if (arguments.length === 0) return this.attr(key); + if (Array.isArray(value)) return this.attr(key, value); + const array = [...(this.attr(key) || []), value]; + return this.attr(key, array); + }; +} + +function defineObjectProp( + Node: NodeClass, + name: string, + { key: k = name }: NodePropertyDescriptor, +) { + Node.prototype[name] = function (key, value) { + if (arguments.length === 0) return this.attr(k); + if (arguments.length === 1 && typeof key !== 'string') { + return this.attr(k, key); + } + const obj = this.attr(k) || {}; + obj[key] = arguments.length === 1 ? true : value; + return this.attr(k, obj); + }; +} + +function defineMixProp( + Node: NodeClass, + name: string, + descriptor: NodePropertyDescriptor, +) { + Node.prototype[name] = function (key) { + if (arguments.length === 0) return this.attr(name); + if (Array.isArray(key)) return this.attr(name, { items: key }); + if ( + isStrictObject(key) && + (key.title !== undefined || key.items !== undefined) + ) { + return this.attr(name, key); + } + if (key === null || key === false) return this.attr(name, key); + const obj = this.attr(name) || {}; + const { items = [] } = obj; + items.push(key); + obj.items = items; + return this.attr(name, obj); + }; +} + +function defineNodeProp( + Node: NodeClass, + name: string, + { ctor }: NodePropertyDescriptor, +) { + Node.prototype[name] = function (hocMark?) { + const node = this.append(ctor); + if (name === 'mark') { + node.type = hocMark; + } + return node; + }; +} + +function defineContainerProp( + Node: NodeClass, + name: string, + { ctor }: NodePropertyDescriptor, +) { + Node.prototype[name] = function () { + this.type = null; + return this.append(ctor); + }; +} + +/** + * A decorator to define different type of attribute setter or + * getter for current node. + */ +export function defineProps( + descriptors: Record, +) { + return (Node: NodeClass) => { + for (const [name, descriptor] of Object.entries(descriptors)) { + const { type } = descriptor; + if (type === 'value') defineValueProp(Node, name, descriptor); + else if (type === 'array') defineArrayProp(Node, name, descriptor); + else if (type === 'object') defineObjectProp(Node, name, descriptor); + else if (type === 'node') defineNodeProp(Node, name, descriptor); + else if (type === 'container') + defineContainerProp(Node, name, descriptor); + else if (type === 'mix') defineMixProp(Node, name, descriptor); + } + return Node as any; + }; +} + +export function nodeProps( + node: Record any>, +): Record { + return Object.fromEntries( + Object.entries(node).map(([name, ctor]) => [name, { type: 'node', ctor }]), + ); +} diff --git a/src/api/extend.ts b/src/api/extend.ts new file mode 100644 index 0000000000..08299e8f16 --- /dev/null +++ b/src/api/extend.ts @@ -0,0 +1,31 @@ +import { G2Spec } from '../spec'; +import { G2Library } from '../runtime'; +import { Runtime, RuntimeOptions } from './runtime'; +import { MarkOf, CompositionOf as Of } from './types'; +import { MarkNode } from './mark'; +import { CompositionNode } from './composition'; + +type CompositionOf = Of< + Library, + ( + composite?, + ) => CompositionNode< + CompositionOf & MarkOf MarkNode> + > & + MarkOf MarkNode> & + CompositionOf +>; + +export function extend( + Runtime: new (options: RuntimeOptions) => Runtime, + library: Library, +): new (options?: RuntimeOptions) => Runtime & + MarkOf MarkNode> & + CompositionOf { + class Chart extends Runtime { + constructor(options: Omit) { + super({ ...options, lib: library }); + } + } + return Chart as any; +} diff --git a/src/api/mark/base.ts b/src/api/mark.ts similarity index 75% rename from src/api/mark/base.ts rename to src/api/mark.ts index c878def4de..a424145102 100644 --- a/src/api/mark/base.ts +++ b/src/api/mark.ts @@ -1,12 +1,15 @@ import { DisplayObject } from '@antv/g'; -import { Node } from '../node'; -import { G2MarkState, Scale } from '../../runtime'; +import { G2MarkState, Scale } from '../runtime'; +import { Mark as Spec } from '../spec'; +import { Node } from './node'; +import { defineProps } from './define'; +import { markProps } from './props'; +import { PropsOf } from './types'; -export class MarkNode< - Value extends Record = Record, - ParentValue extends Record = Record, - ChildValue extends Record = Record, -> extends Node { +export interface MarkNode extends PropsOf {} + +@defineProps(markProps) +export class MarkNode extends Node { changeData(data: any) { const chart = this.getRoot(); if (!chart) return; diff --git a/src/api/mark/index.ts b/src/api/mark/index.ts deleted file mode 100644 index 4e7b066258..0000000000 --- a/src/api/mark/index.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { CompositeMarkType } from '../../spec'; -import { - Interval, - Rect, - Point, - Area, - Line, - Cell, - Vector, - Link, - Polygon, - Image, - Text, - Box, - LineX, - LineY, - Range, - RangeX, - RangeY, - Connector, - Sankey, - Treemap, - Boxplot, - Density, - Heatmap, - Path, - Shape, - Pack, - ForceGraph, - Tree, - WordCloud, - Composite, - Gauge, - AxisX, - AxisY, - Legends, -} from './mark'; - -export interface Mark { - mark(Composite: CompositeMarkType): Composite; - interval(): Interval; - rect(): Rect; - point(): Point; - area(): Area; - line(): Line; - cell(): Cell; - vector(): Vector; - link(): Link; - polygon(): Polygon; - image(): Image; - text(): Text; - box(): Box; - lineX(): LineX; - lineY(): LineY; - range(): Range; - rangeX(): RangeX; - rangeY(): RangeY; - connector(): Connector; - sankey(): Sankey; - treemap(): Treemap; - boxplot(): Boxplot; - density(): Density; - heatmap(): Heatmap; - path(): Path; - shape(): Shape; - pack(): Pack; - forceGraph(): ForceGraph; - tree(): Tree; - wordCloud(): WordCloud; - gauge(): Gauge; - axisX(): AxisX; - axisY(): AxisY; - legends(): Legends; -} - -export { MarkNode } from './base'; - -export const mark = { - mark: Composite, - interval: Interval, - rect: Rect, - point: Point, - area: Area, - line: Line, - cell: Cell, - vector: Vector, - link: Link, - polygon: Polygon, - image: Image, - text: Text, - box: Box, - lineX: LineX, - lineY: LineY, - range: Range, - rangeX: RangeX, - rangeY: RangeY, - connector: Connector, - sankey: Sankey, - treemap: Treemap, - boxplot: Boxplot, - density: Density, - heatmap: Heatmap, - path: Path, - shape: Shape, - pack: Pack, - forceGraph: ForceGraph, - tree: Tree, - wordCloud: WordCloud, - gauge: Gauge, - axisX: AxisX, - axisY: AxisY, - legends: Legends, -}; diff --git a/src/api/mark/mark.ts b/src/api/mark/mark.ts deleted file mode 100644 index d1e7b9b4d1..0000000000 --- a/src/api/mark/mark.ts +++ /dev/null @@ -1,439 +0,0 @@ -import { - IntervalMark, - RectMark, - AreaMark, - PointMark, - CellMark, - VectorMark, - LinkMark, - PolygonMark, - ImageMark, - TextMark, - BoxMark, - LineMark, - LineXMark, - LineYMark, - RangeMark, - RangeXMark, - RangeYMark, - ConnectorMark, - SankeyMark, - BoxPlotMark, - DensityMark, - HeatmapMark, - PathMark, - ShapeMark, - TreemapMark, - ForceGraphMark, - PackMark, - TreeMark, - WordCloudMark, - CompositeMark, - GaugeMark, - AxisComponent, - LegendComponent, -} from '../../spec'; -import { NodePropertyDescriptor, defineProps } from '../props'; -import { Concrete } from '../types'; -import { MarkNode } from './base'; -import { API, StaticAPI } from './types'; - -export interface Interval extends API, Interval> { - type: 'interval'; -} - -export interface Rect extends API, Rect> { - type: 'rect'; -} - -export interface Point extends API, Point> { - type: 'point'; -} - -export interface Area extends API, Area> { - type: 'area'; -} - -export interface Line extends API, Line> { - type: 'line'; -} - -export interface Cell extends API, Cell> { - type: 'cell'; -} - -export interface Vector extends API, Vector> { - type: 'cell'; -} - -export interface Link extends API, Link> { - type: 'link'; -} - -export interface Polygon extends API, Polygon> { - type: 'polygon'; -} - -export interface Image extends API, Image> { - type: 'image'; -} - -export interface Text extends API, Text> { - type: 'text'; -} - -export interface Box extends API, Box> { - type: 'box'; -} - -export interface LineX extends API, LineX> { - type: 'annotation.lineX'; -} - -export interface LineY extends API, LineY> { - type: 'annotation.lineY'; -} - -export interface Range extends API, Range> { - type: 'annotation.range'; -} - -export interface RangeX extends API, RangeX> { - type: 'annotation.rangeX'; -} - -export interface RangeY extends API, RangeY> { - type: 'annotation.rangeY'; -} - -export interface Connector extends API, Connector> { - type: 'connector'; -} - -export interface Sankey extends API, Sankey> { - type: 'sankey'; -} - -export interface Boxplot extends API, Boxplot> { - type: 'boxplot'; -} - -export interface Density extends API, Density> { - type: 'density'; -} - -export interface Heatmap extends API, Heatmap> { - type: 'heatmap'; -} - -export interface Path extends API, Path> { - type: 'path'; -} - -export interface Shape extends API, Shape> { - type: 'shape'; -} - -export interface Treemap extends API, Treemap> { - type: 'treemap'; -} -export interface Pack extends API, Pack> { - type: 'pack'; -} - -export interface ForceGraph extends API, ForceGraph> { - type: 'forceGraph'; -} - -export interface Tree extends API, Tree> { - type: 'tree'; -} - -export interface WordCloud extends API, WordCloud> { - type: 'wordCloud'; -} - -export interface Gauge extends API, Gauge> { - type: 'gauge'; -} - -export interface Composite extends API, Composite> { - type: 'interval'; -} - -export interface AxisX extends StaticAPI, AxisX> { - type: 'axisX'; -} - -export interface AxisY extends StaticAPI, AxisY> { - type: 'axisY'; -} - -export interface Legends extends StaticAPI, Legends> { - type: 'legends'; -} - -export const props: NodePropertyDescriptor[] = [ - { name: 'encode', type: 'object' }, - { name: 'scale', type: 'object' }, - { name: 'data', type: 'value' }, - { name: 'transform', type: 'array' }, - { name: 'style', type: 'object' }, - { name: 'animate', type: 'object' }, - { name: 'coordinate', type: 'object' }, - { name: 'interaction', type: 'object' }, - { name: 'label', type: 'array', key: 'labels' }, - { name: 'axis', type: 'object' }, - { name: 'legend', type: 'object' }, - { name: 'slider', type: 'object' }, - { name: 'scrollbar', type: 'object' }, - { name: 'state', type: 'object' }, - { name: 'tooltip', type: 'mix' }, -]; - -export const axisProps: NodePropertyDescriptor[] = [ - { name: 'scale', type: 'object' }, - { name: 'transform', type: 'array' }, - { name: 'style', type: 'object' }, - { name: 'state', type: 'object' }, -]; - -export const legendProps = axisProps; - -@defineProps(props) -export class Composite extends MarkNode { - constructor() { - super({}); - } -} - -@defineProps(props) -export class Interval extends MarkNode { - constructor() { - super({}, 'interval'); - } -} - -@defineProps(props) -export class Rect extends MarkNode { - constructor() { - super({}, 'rect'); - } -} - -@defineProps(props) -export class Point extends MarkNode { - constructor() { - super({}, 'point'); - } -} - -@defineProps(props) -export class Area extends MarkNode { - constructor() { - super({}, 'area'); - } -} - -@defineProps(props) -export class Line extends MarkNode { - constructor() { - super({}, 'line'); - } -} - -@defineProps(props) -export class Cell extends MarkNode { - constructor() { - super({}, 'cell'); - } -} - -@defineProps(props) -export class Vector extends MarkNode { - constructor() { - super({}, 'vector'); - } -} - -@defineProps(props) -export class Link extends MarkNode { - constructor() { - super({}, 'link'); - } -} - -@defineProps(props) -export class Polygon extends MarkNode { - constructor() { - super({}, 'polygon'); - } -} - -@defineProps(props) -export class Image extends MarkNode { - constructor() { - super({}, 'image'); - } -} - -@defineProps(props) -export class Text extends MarkNode { - constructor() { - super({}, 'text'); - } -} - -@defineProps(props) -export class Box extends MarkNode { - constructor() { - super({}, 'box'); - } -} - -@defineProps(props) -export class LineX extends MarkNode { - constructor() { - super({}, 'lineX'); - } -} - -@defineProps(props) -export class LineY extends MarkNode { - constructor() { - super({}, 'lineY'); - } -} - -@defineProps(props) -export class Range extends MarkNode { - constructor() { - super({}, 'range'); - } -} - -@defineProps(props) -export class RangeX extends MarkNode { - constructor() { - super({}, 'rangeX'); - } -} - -@defineProps(props) -export class RangeY extends MarkNode { - constructor() { - super({}, 'rangeY'); - } -} - -@defineProps(props) -export class Connector extends MarkNode { - constructor() { - super({}, 'connector'); - } -} - -@defineProps(props) -export class Shape extends MarkNode { - constructor() { - super({}, 'shape'); - } -} - -@defineProps([...props, { name: 'layout', type: 'value' }]) -export class Sankey extends MarkNode { - constructor() { - super({}, 'sankey'); - } -} - -@defineProps([...props, { name: 'layout', type: 'value' }]) -export class Treemap extends MarkNode { - constructor() { - super({}, 'treemap'); - } -} - -@defineProps(props) -export class Boxplot extends MarkNode { - constructor() { - super({}, 'boxplot'); - } -} - -@defineProps(props) -export class Density extends MarkNode { - constructor() { - super({}, 'density'); - } -} - -@defineProps(props) -export class Heatmap extends MarkNode { - constructor() { - super({}, 'heatmap'); - } -} - -@defineProps(props) -export class Path extends MarkNode { - constructor() { - super({}, 'path'); - } -} - -@defineProps([...props, { name: 'layout', type: 'value' }]) -export class Pack extends MarkNode { - constructor() { - super({}, 'pack'); - } -} - -@defineProps([...props, { name: 'layout', type: 'value' }]) -export class ForceGraph extends MarkNode { - constructor() { - super({}, 'forceGraph'); - } -} - -@defineProps([...props, { name: 'layout', type: 'value' }]) -export class Tree extends MarkNode { - constructor() { - super({}, 'tree'); - } -} - -@defineProps([...props, { name: 'layout', type: 'object' }]) -export class WordCloud extends MarkNode { - constructor() { - super({}, 'wordCloud'); - } -} - -@defineProps(props) -export class Gauge extends MarkNode { - constructor() { - super({}, 'gauge'); - } -} - -@defineProps(axisProps) -export class AxisX extends MarkNode { - constructor() { - super({}, 'axisX'); - } -} - -@defineProps(axisProps) -export class AxisY extends MarkNode { - constructor() { - super({}, 'axisY'); - } -} - -@defineProps(legendProps) -export class Legends extends MarkNode { - constructor() { - super({}, 'legends'); - } -} diff --git a/src/api/mark/types.ts b/src/api/mark/types.ts deleted file mode 100644 index c9e588f1c7..0000000000 --- a/src/api/mark/types.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ValueAttribute, ObjectAttribute, ArrayAttribute } from '../types'; -import { Mark as MarkProps, AxisComponent, LegendComponent } from '../../spec'; - -export type API = { - data: ValueAttribute; - encode: ObjectAttribute; - scale: ObjectAttribute; - transform: ArrayAttribute; - animate: ObjectAttribute; - label: ArrayAttribute; - style: ObjectAttribute; - state: ObjectAttribute; - axis: ObjectAttribute; - coordinate: ValueAttribute; - interaction: ObjectAttribute; - slider: ObjectAttribute; - scrollbar: ObjectAttribute; - legend: ObjectAttribute; - layout: ValueAttribute; - tooltip: ValueAttribute; -}; - -export type StaticAPI< - Props extends AxisComponent | LegendComponent, - StaticMark, -> = { - scale: ObjectAttribute; - transform: ArrayAttribute; - style: ObjectAttribute; - state: ObjectAttribute; -}; diff --git a/src/api/node.ts b/src/api/node.ts index db81a8738f..1691e6ffb4 100644 --- a/src/api/node.ts +++ b/src/api/node.ts @@ -1,4 +1,4 @@ -import { Chart } from './chart'; +import { Runtime } from './runtime'; /** * BFS nodes and execute callback. @@ -140,12 +140,12 @@ export class Node< return this; } - getRoot(): Chart { + getRoot(): Runtime { // Find the root chart and render. let root: Node = this; while (root && root.parentNode) { root = root.parentNode; } - return root as Chart; + return root as Runtime; } } diff --git a/src/api/props.ts b/src/api/props.ts index c49024fa3d..115d8b0c00 100644 --- a/src/api/props.ts +++ b/src/api/props.ts @@ -1,114 +1,30 @@ -import { isStrictObject } from '../utils/helper'; - -export type NodePropertyDescriptor = { - type: 'object' | 'value' | 'array' | 'node' | 'container' | 'mix'; - name: string; - key?: string; - ctor?: new (...args: any[]) => any; -}; - -function defineValueProp(Node, { name, key = name }: NodePropertyDescriptor) { - Node.prototype[name] = function (value) { - if (arguments.length === 0) return this.attr(key); - return this.attr(key, value); - }; -} - -function defineArrayProp(Node, { name, key = name }: NodePropertyDescriptor) { - Node.prototype[name] = function (value) { - if (arguments.length === 0) return this.attr(key); - if (Array.isArray(value)) return this.attr(key, value); - const array = [...(this.attr(key) || []), value]; - return this.attr(key, array); - }; -} - -function defineObjectProp( - Node, - { name, key: k = name }: NodePropertyDescriptor, -) { - Node.prototype[name] = function (key, value) { - if (arguments.length === 0) return this.attr(k); - if (arguments.length === 1 && typeof key !== 'string') { - return this.attr(k, key); - } - const obj = this.attr(k) || {}; - obj[key] = arguments.length === 1 ? true : value; - return this.attr(k, obj); - }; -} - -function defineMixProp(Node, { name }: NodePropertyDescriptor) { - Node.prototype[name] = function (key) { - if (arguments.length === 0) return this.attr(name); - if (Array.isArray(key)) return this.attr(name, { items: key }); - if ( - isStrictObject(key) && - (key.title !== undefined || key.items !== undefined) - ) { - return this.attr(name, key); - } - if (key === null || key === false) return this.attr(name, key); - const obj = this.attr(name) || {}; - const { items = [] } = obj; - items.push(key); - obj.items = items; - return this.attr(name, obj); - }; -} - -function defineNodeProp(Node, { name, ctor }: NodePropertyDescriptor) { - Node.prototype[name] = function (hocMark?) { - const node = this.append(ctor); - if (name === 'mark') { - node.type = hocMark; - } - return node; - }; -} - -function defineContainerProp(Node, { name, ctor }: NodePropertyDescriptor) { - Node.prototype[name] = function () { - this.type = null; - return this.append(ctor); - }; -} - -/** - * A decorator to define different type of attribute setter or - * getter for current node. - */ -export function defineProps(descriptors: NodePropertyDescriptor[]) { - return (Node) => { - for (const descriptor of descriptors) { - const { type } = descriptor; - if (type === 'value') defineValueProp(Node, descriptor); - else if (type === 'array') defineArrayProp(Node, descriptor); - else if (type === 'object') defineObjectProp(Node, descriptor); - else if (type === 'node') defineNodeProp(Node, descriptor); - else if (type === 'container') defineContainerProp(Node, descriptor); - else if (type === 'mix') defineMixProp(Node, descriptor); - } - return Node; - }; -} - -export function nodeProps( - node: Record any>, -): NodePropertyDescriptor[] { - return Object.entries(node).map(([name, ctor]) => ({ - type: 'node', - name, - ctor, - })); -} - -export function containerProps( - node: Record any>, -): NodePropertyDescriptor[] { - return Object.entries(node).map(([name, ctor]) => ({ - type: 'container', - name, - ctor, - })); -} +export const commonProps = { + encode: { type: 'object' }, + scale: { type: 'object' }, + data: { type: 'value' }, + transform: { type: 'array' }, + style: { type: 'object' }, + animate: { type: 'object' }, + coordinate: { type: 'object' }, + interaction: { type: 'object' }, + label: { type: 'array', key: 'labels' }, + axis: { type: 'object' }, + legend: { type: 'object' }, + slider: { type: 'object' }, + scrollbar: { type: 'object' }, + state: { type: 'object' }, + layout: { type: 'object' }, + theme: { type: 'object' }, + title: { type: 'value' }, +} as const; + +export const markProps = { + ...commonProps, + tooltip: { type: 'mix' }, + viewStyle: { type: 'object' }, +} as const; + +export const compositionProps = { + ...commonProps, + labelTransform: { type: 'array' }, +} as const; diff --git a/src/api/runtime.ts b/src/api/runtime.ts new file mode 100644 index 0000000000..3d6cd5150b --- /dev/null +++ b/src/api/runtime.ts @@ -0,0 +1,362 @@ +import { IRenderer, RendererPlugin, Canvas as GCanvas } from '@antv/g'; +import { Renderer as CanvasRenderer } from '@antv/g-canvas'; +import { Plugin as DragAndDropPlugin } from '@antv/g-plugin-dragndrop'; +import { debounce } from '@antv/util'; +import EventEmitter from '@antv/event-emitter'; +import { G2Context, render, destroy } from '../runtime'; +import { G2Spec, ViewComposition } from '../spec'; +import { ChartEvent } from '../utils/event'; +import { G2Library } from '../runtime/types/options'; +import { + normalizeContainer, + removeContainer, + sizeOf, + optionsOf, + updateRoot, + createEmptyPromise, +} from './utils'; +import { CompositionNode } from './composition'; +import { Node } from './node'; +import { defineProps, nodeProps } from './define'; +import { MarkNode } from './mark'; + +export const G2_CHART_KEY = 'G2_CHART_KEY'; + +export type RuntimeOptions = ViewComposition & { + container?: string | HTMLElement; + canvas?: GCanvas; + autoFit?: boolean; + renderer?: IRenderer; + plugins?: RendererPlugin[]; + theme?: string; + lib?: G2Library; +}; + +export class Runtime extends CompositionNode { + private _container: HTMLElement; + private _context: G2Context; + private _emitter: EventEmitter; + private _width: number; + private _height: number; + private _renderer: IRenderer; + private _plugins: RendererPlugin[]; + // Identifies whether bindAutoFit. + private _hasBindAutoFit = false; + private _rendering = false; + private _trailing = false; + private _trailingResolve = null; + private _trailingReject = null; + private _previousDefinedType = null; + private _marks: Record Node>; + private _compositions: Record Node>; + + constructor(options: RuntimeOptions) { + const { container, canvas, renderer, plugins, lib, ...rest } = options; + super(rest, 'view'); + this._renderer = renderer || new CanvasRenderer(); + this._plugins = plugins || []; + this._container = normalizeContainer(container); + this._emitter = new EventEmitter(); + this._context = { library: lib, emitter: this._emitter, canvas }; + this._create(); + } + + render(): Promise> { + if (this._rendering) return this._addToTrailing(); + if (!this._context.canvas) this._createCanvas(); + this._bindAutoFit(); + this._rendering = true; + const finished = new Promise>((resolve, reject) => + render( + this._computedOptions(), + this._context, + this._createResolve(resolve), + this._createReject(reject), + ), + ); + const [finished1, resolve, reject] = createEmptyPromise>(); + finished + .then(resolve) + .catch(reject) + .then(() => this._renderTrailing()); + return finished1; + } + + /** + * @overload + * @returns {Spec} + */ + options(): Spec; + /** + * @overload + * @param {G2ViewTree} options + * @returns {Runtime} + */ + options(options: Spec): Runtime; + /** + * @overload + * @param {G2ViewTree} [options] + * @returns {Runtime|Spec} + */ + options(options?: Spec): Runtime | Spec { + if (arguments.length === 0) return optionsOf(this) as Spec; + const { type } = options; + if (type) this._previousDefinedType = type; + updateRoot( + this, + options, + this._previousDefinedType, + this._marks, + this._compositions, + ); + return this; + } + + getContainer(): HTMLElement { + return this._container; + } + + getContext(): G2Context { + return this._context; + } + + on(event: string, callback: (...args: any[]) => any, once?: boolean): this { + this._emitter.on(event, callback, once); + return this; + } + + once(event: string, callback: (...args: any[]) => any): this { + this._emitter.once(event, callback); + return this; + } + + emit(event: string, ...args: any[]): this { + this._emitter.emit(event, ...args); + return this; + } + + off(event?: string, callback?: (...args: any[]) => any) { + this._emitter.off(event, callback); + return this; + } + + clear() { + const options = this.options(); + this.emit(ChartEvent.BEFORE_CLEAR); + this._reset(); + destroy(options, this._context, false); + this.emit(ChartEvent.AFTER_CLEAR); + } + + destroy() { + const options = this.options(); + this.emit(ChartEvent.BEFORE_DESTROY); + this._unbindAutoFit(); + this._reset(); + destroy(options, this._context, true); + removeContainer(this._container); + this.emit(ChartEvent.AFTER_DESTROY); + } + + forceFit() { + // Don't fit if size do not change. + this.options['autoFit'] = true; + const { width, height } = sizeOf(this.options(), this._container); + if (width === this._width && height === this._height) { + return Promise.resolve(this); + } + + // Don't call changeSize to prevent update width and height of options. + this.emit(ChartEvent.BEFORE_CHANGE_SIZE); + const finished = this.render(); + finished.then(() => { + this.emit(ChartEvent.AFTER_CHANGE_SIZE); + }); + return finished; + } + + changeSize(width: number, height: number): Promise> { + if (width === this._width && height === this._height) { + return Promise.resolve(this); + } + this.emit(ChartEvent.BEFORE_CHANGE_SIZE); + this.attr('width', width); + this.attr('height', height); + const finished = this.render(); + finished.then(() => { + this.emit(ChartEvent.AFTER_CHANGE_SIZE); + }); + return finished; + } + + private _create() { + const { library } = this._context; + + // @todo After refactor component as mark, remove this. + const isMark = (key) => + key.startsWith('mark.') || + key === 'component.axisX' || + key === 'component.axisY' || + key === 'component.legends'; + const marks = Object.keys(library).filter(isMark); + + // Create mark generators. + this._marks = {}; + for (const key of marks) { + const name = key.split('.').pop(); + class Mark extends MarkNode { + constructor() { + super({}, name); + } + } + this._marks[name] = Mark; + this[name] = function (composite) { + const node = this.append(Mark); + if (name === 'mark') node.type = composite; + return node; + }; + } + + // Create composition generators. + const compositions = Object.keys(library).filter((key) => + key.startsWith('composition.'), + ); + this._compositions = Object.fromEntries( + compositions.map((key) => { + const name = key.split('.').pop(); + @defineProps(nodeProps(this._marks)) + class Composition extends CompositionNode { + constructor() { + super({}, name); + } + } + return [name, Composition]; + }), + ); + + for (const Ctor of Object.values(this._compositions)) { + defineProps(nodeProps(this._compositions))(Ctor); + } + + for (const key of compositions) { + const name = key.split('.').pop(); + this[name] = function () { + const Composition = this._compositions[name]; + this.type = null; + return this.append(Composition); + }; + } + } + + private _reset() { + const KEYS = ['theme', 'type', 'width', 'height', 'autoFit']; + this.type = 'view'; + this.value = Object.fromEntries( + Object.entries(this.value).filter( + ([key]) => + key.startsWith('margin') || + key.startsWith('padding') || + key.startsWith('inset') || + KEYS.includes(key), + ), + ); + 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: Runtime) => 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> { + // Resolve previous promise, and give up this task. + this._trailingResolve?.(this); + + // Create new task. + this._trailing = true; + const promise = new Promise>((resolve, reject) => { + this._trailingResolve = resolve; + this._trailingReject = reject; + }); + + return promise; + } + + private _onResize = debounce(() => { + this.forceFit(); + }, 300); + + private _bindAutoFit() { + const options = this.options(); + const { autoFit } = options; + + if (this._hasBindAutoFit) { + // If it was bind before, unbind it now. + if (!autoFit) this._unbindAutoFit(); + return; + } + + if (autoFit) { + this._hasBindAutoFit = true; + window.addEventListener('resize', this._onResize); + } + } + + private _unbindAutoFit() { + if (this._hasBindAutoFit) { + this._hasBindAutoFit = false; + window.removeEventListener('resize', this._onResize); + } + } +} diff --git a/src/api/types.ts b/src/api/types.ts index 45b81d4097..5bf4a1a1c7 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -1,24 +1,102 @@ +type ElementOf = T extends Array ? T[number] : never; + type ValueOf = T[keyof T]; -type ElementOf = T extends Array ? T[number] : never; +export type Concrete = { [Key in keyof T]-?: T[Key] }; -type Chainable = K extends never ? V : N; +/** + * If typeof `Value` is value, returns `Node`. + * Otherwise returns the value. + */ +export type ValueAttribute = (() => Value) & + ((value: Value) => Node); -export type ValueAttribute = ( - value?: T, -) => Chainable; +/** + * If no params, returns `Value`. + * Otherwise returns the `Node`. + */ +export type ObjectAttribute = (() => Value) & + ( ? Value : never>( + key: K | keyof K | boolean, + ) => Node) & + (< + K extends Value extends Record ? keyof Value : never, + V extends Value extends Record ? ValueOf : never, + >( + key: K, + value: V | boolean, + ) => Node); -export type ObjectAttribute = < - T extends V extends Record ? ValueOf : V, ->( - key?: V extends Record ? V | keyof V : V, - value?: T, -) => V extends Record | boolean - ? Chainable // For mark.axis({ x: {} }) and mark.axis(false) - : Chainable; // For mark.axis('x', {}) +/** + * If typeof `Value` is an array or an element of array, returns `Node`. + * Otherwise returns the array. + */ +export type ArrayAttribute = (() => Value) & + ((value: Value | ElementOf) => Node); -export type ArrayAttribute = >( - value?: T, -) => Chainable; +/** + * Filter Object keys with the specified prefix. + * @example + * type A = { 'mark.a': number, 'composition.b': number } + * type B = FilterKey; // { 'a': number } + */ +export type FilterKey< + T extends string | symbol | number, + Prefix extends string, +> = T extends `${Prefix}.${infer Key}` ? Key : never; -export type Concrete = { [Key in keyof T]-?: T[Key] }; +/** + * Filter Object keys with `mark` as prefix. + * @example + * type A = { 'mark.a': number, 'composition.b': number } + * type B = FilterKey; // { 'a': number } + * @todo Remove component.xxxx + */ +export type FilterMark = + T extends 'component.axisX' + ? 'axisX' + : T extends 'component.axisY' + ? 'axisY' + : T extends 'component.legends' + ? 'legends' + : FilterKey; + +/** + * Filter Object keys with `composition` as prefix. + * @example + * type A = { 'mark.a': number, 'composition.b': number } + * type B = FilterComposition; // { 'b': number } + */ +export type FilterComposition = FilterKey< + T, + 'composition' +>; + +/** + * Map marks of library to Nodes. + */ +export type MarkOf, Node> = { + [Key in keyof Library as FilterMark]: Node; +}; + +/** + * Map compositions of library to Nodes. + */ +export type CompositionOf, Node> = { + [Key in keyof Library as FilterComposition]: Node; +}; + +/** + * Map descriptors to props of Nodes. + */ +export type PropsOf< + Descriptor extends Record, + Spec extends Record, + Node, +> = { + [Key in keyof Descriptor]: Descriptor[Key] extends { type: 'object' } + ? ObjectAttribute + : Descriptor[Key] extends { type: 'value' } + ? ValueAttribute + : ArrayAttribute; +}; diff --git a/src/api/utils.ts b/src/api/utils.ts index 2f16266f47..b658ef5373 100644 --- a/src/api/utils.ts +++ b/src/api/utils.ts @@ -2,8 +2,6 @@ import { G2ViewTree } from '../runtime'; import { getContainerSize } from '../utils/size'; import { deepAssign } from '../utils/helper'; import { Node } from './node'; -import { mark } from './mark'; -import { composition } from './composition'; // Keys can specified by new Chart({...}). // Keys can bubble form mark-level options to view-level options. @@ -90,7 +88,10 @@ export function optionsOf(node: Node): Record { return nodeValue.get(root); } -function isMark(type: string | ((...args: any[]) => any)): boolean { +function isMark( + type: string | ((...args: any[]) => any), + mark: Record Node>, +): boolean { if (typeof type === 'function') return true; return new Set(Object.keys(mark)).has(type); } @@ -99,11 +100,12 @@ function normalizeRootOptions( node: Node, options: G2ViewTree, previousType: string, + marks: Record Node>, ) { const { type: oldType } = node; const { type = previousType || oldType } = options; if (type === 'view') return options; - if (!isMark(type)) return options; + if (!isMark(type, marks)) return options; const view = { type: 'view' }; const mark = { ...options }; for (const key of VIEW_KEYS) { @@ -115,7 +117,11 @@ function normalizeRootOptions( return { ...view, children: [mark] }; } -function typeCtor(type: string | ((...args: any[]) => any)): new () => Node { +function typeCtor( + type: string | ((...args: any[]) => any), + mark: Record Node>, + composition: Record Node>, +): new () => Node { if (typeof type === 'function') return mark.mark; const node = { ...mark, ...composition }; const ctor = node[type]; @@ -124,9 +130,13 @@ function typeCtor(type: string | ((...args: any[]) => any)): new () => Node { } // Create node from options. -function createNode(options: G2ViewTree): Node { +function createNode( + options: G2ViewTree, + mark: Record Node>, + composition: Record Node>, +): Node { const { type, children, ...value } = options; - const Ctor = typeCtor(type); + const Ctor = typeCtor(type, mark, composition); const node = new Ctor(); node.value = value; // @ts-ignore @@ -148,12 +158,17 @@ function updateNode(node: Node, newOptions: G2ViewTree) { } // Create a nested node tree from newOptions, and append it to the parent. -function appendNode(parent: Node, newOptions: G2ViewTree) { +function appendNode( + parent: Node, + newOptions: G2ViewTree, + mark: Record Node>, + composition: Record Node>, +) { if (!parent) return; const discovered = [[parent, newOptions]]; while (discovered.length) { const [parent, nodeOptions] = discovered.shift(); - const node = createNode(nodeOptions); + const node = createNode(nodeOptions, mark, composition); if (Array.isArray(parent.children)) parent.push(node); const { children } = nodeOptions; if (Array.isArray(children)) { @@ -169,14 +184,16 @@ export function updateRoot( node: Node, options: G2ViewTree, definedType: string, + mark: Record Node>, + composition: Record Node>, ) { - const rootOptions = normalizeRootOptions(node, options, definedType); + const rootOptions = normalizeRootOptions(node, options, definedType, mark); const discovered: [Node, Node, G2ViewTree][] = [[null, node, rootOptions]]; while (discovered.length) { const [parent, oldNode, newNode] = discovered.shift(); // If there is no oldNode, create a node tree directly. if (!oldNode) { - appendNode(parent, newNode); + appendNode(parent, newNode, mark, composition); } else if (!newNode) { oldNode.remove(); } else { diff --git a/src/runtime/index.ts b/src/runtime/index.ts index 48c8526d7b..67259f60e3 100644 --- a/src/runtime/index.ts +++ b/src/runtime/index.ts @@ -1,5 +1,4 @@ export * from './constant'; -export { render, renderToMountedElement, destroy } from './render'; export * from './types/common'; export * from './types/component'; export * from './types/options'; @@ -7,3 +6,4 @@ export * from './types/transform'; export * from './types/encode'; export * from './types/mark'; export * from './types/data'; +export { render, renderToMountedElement, destroy } from './render'; diff --git a/src/runtime/render.ts b/src/runtime/render.ts index 24e457b2a6..8f03b53c05 100644 --- a/src/runtime/render.ts +++ b/src/runtime/render.ts @@ -3,7 +3,6 @@ import { Renderer as CanvasRenderer } from '@antv/g-canvas'; import { Plugin as DragAndDropPlugin } from '@antv/g-plugin-dragndrop'; import { deepMix } from '@antv/util'; import EventEmitter from '@antv/event-emitter'; -import { createLibrary } from '../stdlib'; import { select } from '../utils/selection'; import { ChartEvent } from '../utils/event'; import { error } from '../utils/helper'; @@ -80,11 +79,10 @@ export function render( const keyed = inferKeys(options); const { canvas = Canvas(width, height), - library = createLibrary(), emitter = new EventEmitter(), + library, } = context; context.canvas = canvas; - context.library = library; context.emitter = emitter; canvas.resize(width, height); @@ -121,12 +119,12 @@ export function renderToMountedElement( }, ): DisplayObject { // Initialize the context if it is not provided. - const { width = 640, height = 480, on } = options; + const { width = 640, height = 480 } = options; const keyed = inferKeys(options); const { - library = createLibrary(), group = new Group(), emitter = new EventEmitter(), + library, } = context; if (!group?.parentElement) { @@ -135,7 +133,6 @@ export function renderToMountedElement( const selection = select(group); context.group = group; - context.library = library; context.emitter = emitter; emitter.emit(ChartEvent.BEFORE_RENDER); diff --git a/src/runtime/types/component.ts b/src/runtime/types/component.ts index 377f1b3aed..89f423a1b2 100644 --- a/src/runtime/types/component.ts +++ b/src/runtime/types/component.ts @@ -67,7 +67,7 @@ export type G2ComponentValue = export type G2BaseComponent< R = any, - O = Record | void, + O = Record, P = Record, C = Record, > = { diff --git a/src/runtime/types/options.ts b/src/runtime/types/options.ts index 17c5d3b20f..7e3da53423 100644 --- a/src/runtime/types/options.ts +++ b/src/runtime/types/options.ts @@ -43,7 +43,7 @@ export type Node = { export type G2Library = Record< `${G2ComponentNamespaces}.${string}`, - G2BaseComponent + (...args: any[]) => any >; // @todo diff --git a/src/spec/composition.ts b/src/spec/composition.ts index a51bb042a3..65ec111a93 100644 --- a/src/spec/composition.ts +++ b/src/spec/composition.ts @@ -93,6 +93,7 @@ export type GeoViewComposition = Omit & { type?: 'geoView'; // @todo coordinate?: Record; + [key: string]: any; // @todo }; export type GeoPathComposition = Omit & { @@ -107,6 +108,7 @@ export type SpaceLayerComposition = { key?: string; data?: any; children?: Node[]; + [key: string]: any; // @todo }; export type SpaceFlexComposition = { @@ -117,6 +119,7 @@ export type SpaceFlexComposition = { ratio?: number[]; padding?: Padding; children?: Node[]; + [key: string]: any; // @todo }; export type FacetContext = { @@ -162,6 +165,7 @@ export type FacetRectComposition = { axis?: Record | boolean; // @todo legend?: Record | boolean; + [key: string]: any; // @todo }; export type RepeatMatrixComposition = { @@ -195,6 +199,7 @@ export type RepeatMatrixComposition = { // @todo legend?: Record | boolean; children?: Node[] | ((facet: FacetContext) => Node); + [key: string]: any; // @todo }; export type FacetCircleComposition = { @@ -226,6 +231,7 @@ export type FacetCircleComposition = { axis?: Record | boolean; // @todo legend?: Record | boolean; + [key: string]: any; // @todo }; export type TimingKeyframeComposition = { @@ -236,6 +242,7 @@ export type TimingKeyframeComposition = { iterationCount?: 'infinite' | number; direction?: 'normal' | 'reverse' | 'alternate' | 'reverse-alternate'; children?: Node[]; + [key: string]: any; // @todo }; type Node = Mark | Composition; diff --git a/src/spec/index.ts b/src/spec/index.ts index 20ab48892f..4c2a32da86 100644 --- a/src/spec/index.ts +++ b/src/spec/index.ts @@ -5,6 +5,7 @@ import { Mark } from './mark'; export type G2Spec = (Mark | Composition | AxisComponent | LegendComponent) & { width?: number; height?: number; + autoFit?: boolean; }; export * from './animate'; diff --git a/src/spec/interaction.ts b/src/spec/interaction.ts index 18a420d9af..3d0be37f08 100644 --- a/src/spec/interaction.ts +++ b/src/spec/interaction.ts @@ -128,6 +128,7 @@ export type ElementHighlightByXInteraction = { link?: boolean; background?: boolean; offset?: number; + delay?: number; } & Record<`${'link' | 'background'}${any}`, any>; export type ElementHighlightByColorInteraction = { @@ -136,6 +137,7 @@ export type ElementHighlightByColorInteraction = { background?: boolean; link?: boolean; offset?: number; + delay?: number; } & Record<`${'link' | 'background'}${any}`, any>; export type PoptipInteraction = { diff --git a/src/stdlib/index.ts b/src/stdlib/index.ts index b6173effb9..f9b014a740 100644 --- a/src/stdlib/index.ts +++ b/src/stdlib/index.ts @@ -1,4 +1,3 @@ -import { G2Library } from '../runtime'; import { Cartesian, Polar, @@ -257,7 +256,7 @@ import { } from '../label-transform'; import { geoLibrary } from '../geo'; -export function createLibrary(): G2Library { +export function createLibrary() { return { 'data.fetch': Fetch, 'data.inline': Inline, @@ -509,5 +508,5 @@ export function createLibrary(): G2Library { 'labelTransform.overflowHide': OverflowHide, 'labelTransform.contrastReverse': ContrastReverse, ...geoLibrary, - }; + } as const; }