diff --git a/__tests__/main.ts b/__tests__/main.ts
index 42c7d698d1..951dcb3a19 100644
--- a/__tests__/main.ts
+++ b/__tests__/main.ts
@@ -1,8 +1,6 @@
import { Canvas } from '@antv/g';
import { Renderer as CanvasRenderer } from '@antv/g-canvas';
import { Plugin as DragAndDropPlugin } from '@antv/g-plugin-dragndrop';
-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 { stdlib, render } from '../src';
@@ -151,10 +149,6 @@ function createSpecRender(object) {
renderer.registerPlugin(
new DragAndDropPlugin({ dragstartDistanceThreshold: 1 }),
);
- if (selectRenderer.value === 'webgl') {
- renderer.registerPlugin(new ControlPlugin());
- renderer.registerPlugin(new ThreeDPlugin());
- }
canvas = new Canvas({
container: document.createElement('div'),
width,
diff --git a/__tests__/plots/api/chart-render-3d-scatter-plot-perspective.ts b/__tests__/plots/api/chart-render-3d-scatter-plot-perspective.ts
new file mode 100644
index 0000000000..0cfc969a6f
--- /dev/null
+++ b/__tests__/plots/api/chart-render-3d-scatter-plot-perspective.ts
@@ -0,0 +1,65 @@
+import { CameraType } from '@antv/g';
+import { Renderer as WebGLRenderer } from '@antv/g-webgl';
+import { Plugin as ThreeDPlugin, DirectionalLight } from '@antv/g-plugin-3d';
+import { Plugin as ControlPlugin } from '@antv/g-plugin-control';
+import { Runtime, extend } from '../../../src/api';
+import { corelib, threedlib } from '../../../src/lib';
+
+export function chartRender3dScatterPlotPerspective(context) {
+ const { container } = context;
+
+ // Create a WebGL renderer.
+ const renderer = new WebGLRenderer();
+ renderer.registerPlugin(new ThreeDPlugin());
+ renderer.registerPlugin(new ControlPlugin());
+
+ const Chart = extend(Runtime, { ...corelib(), ...threedlib() });
+ const chart = new Chart({
+ container,
+ theme: 'classic',
+ renderer,
+ depth: 400,
+ });
+
+ chart
+ .point3D()
+ .data({
+ type: 'fetch',
+ value: 'data/cars2.csv',
+ })
+ .encode('x', 'Horsepower')
+ .encode('y', 'Miles_per_Gallon')
+ .encode('z', 'Weight_in_lbs')
+ .encode('size', 'Origin')
+ .encode('color', 'Cylinders')
+ .encode('shape', 'cube')
+ .coordinate({ type: 'cartesian3D' })
+ .scale('x', { nice: true })
+ .scale('y', { nice: true })
+ .scale('z', { nice: true })
+ .legend(false)
+ .axis('x', { gridLineWidth: 2 })
+ .axis('y', { gridLineWidth: 2, titleBillboardRotation: -Math.PI / 2 })
+ .axis('z', { gridLineWidth: 2 });
+
+ const finished = chart.render().then(() => {
+ const { canvas } = chart.getContext();
+ const camera = canvas!.getCamera();
+ camera.setPerspective(0.1, 5000, 45, 500 / 500);
+ camera.setType(CameraType.ORBITING);
+
+ // Add a directional light into scene.
+ const light = new DirectionalLight({
+ style: {
+ intensity: 3,
+ fill: 'white',
+ direction: [-1, 0, 1],
+ },
+ });
+ canvas!.appendChild(light);
+ });
+
+ return { finished };
+}
+
+chartRender3dScatterPlotPerspective.skip = true;
diff --git a/__tests__/plots/api/chart-render-3d-scatter-plot.ts b/__tests__/plots/api/chart-render-3d-scatter-plot.ts
new file mode 100644
index 0000000000..6046b75f91
--- /dev/null
+++ b/__tests__/plots/api/chart-render-3d-scatter-plot.ts
@@ -0,0 +1,65 @@
+import { CameraType } from '@antv/g';
+import { Renderer as WebGLRenderer } from '@antv/g-webgl';
+import { Plugin as ThreeDPlugin, DirectionalLight } from '@antv/g-plugin-3d';
+import { Plugin as ControlPlugin } from '@antv/g-plugin-control';
+import { Runtime, extend } from '../../../src/api';
+import { corelib, threedlib } from '../../../src/lib';
+
+export function chartRender3dScatterPlot(context) {
+ const { container } = context;
+
+ // Create a WebGL renderer.
+ const renderer = new WebGLRenderer();
+ renderer.registerPlugin(new ThreeDPlugin());
+ renderer.registerPlugin(new ControlPlugin());
+
+ const Chart = extend(Runtime, { ...corelib(), ...threedlib() });
+ const chart = new Chart({
+ container,
+ theme: 'classic',
+ renderer,
+ depth: 400,
+ });
+
+ chart
+ .point3D()
+ .data({
+ type: 'fetch',
+ value: 'data/cars2.csv',
+ })
+ .encode('x', 'Horsepower')
+ .encode('y', 'Miles_per_Gallon')
+ .encode('z', 'Weight_in_lbs')
+ .encode('size', 'Origin')
+ .encode('color', 'Cylinders')
+ .encode('shape', 'cube')
+ .coordinate({ type: 'cartesian3D' })
+ .scale('x', { nice: true })
+ .scale('y', { nice: true })
+ .scale('z', { nice: true })
+ .legend(false)
+ .axis('x', { gridLineWidth: 2 })
+ .axis('y', { gridLineWidth: 2, titleBillboardRotation: -Math.PI / 2 })
+ .axis('z', { gridLineWidth: 2 });
+
+ const finished = chart.render().then(() => {
+ const { canvas } = chart.getContext();
+ const camera = canvas!.getCamera();
+ camera.setType(CameraType.ORBITING);
+ camera.rotate(-20, -20, 0);
+
+ // Add a directional light into scene.
+ const light = new DirectionalLight({
+ style: {
+ intensity: 2.5,
+ fill: 'white',
+ direction: [-1, 0, 1],
+ },
+ });
+ canvas!.appendChild(light);
+ });
+
+ return { finished };
+}
+
+chartRender3dScatterPlot.skip = true;
diff --git a/__tests__/plots/api/index.ts b/__tests__/plots/api/index.ts
index 6ea8ebf4bd..3ff7aad970 100644
--- a/__tests__/plots/api/index.ts
+++ b/__tests__/plots/api/index.ts
@@ -41,3 +41,5 @@ export { chartChangeDataLegend } from './chart-change-data-legend';
export { chartTooltipMultiChart } from './chart-tooltip-multi-chart';
export { chartOnTextClick } from './chart-on-text-click';
export { chartRenderEvent } from './chart-render-event';
+export { chartRender3dScatterPlot } from './chart-render-3d-scatter-plot';
+export { chartRender3dScatterPlotPerspective } from './chart-render-3d-scatter-plot-perspective';
diff --git a/__tests__/unit/coordinate/index.spec.ts b/__tests__/unit/coordinate/index.spec.ts
index 433fe91b7b..ba5fec4f84 100644
--- a/__tests__/unit/coordinate/index.spec.ts
+++ b/__tests__/unit/coordinate/index.spec.ts
@@ -4,6 +4,7 @@ import {
Transpose,
Parallel,
Fisheye,
+ Cartesian3D,
} from '../../../src/coordinate';
describe('coordinate', () => {
@@ -50,4 +51,8 @@ describe('coordinate', () => {
it('Fisheye({...}) returns expected coordinate transformations', () => {
expect(Fisheye({})).toEqual([['fisheye', 0, 0, 2, 2, false]]);
});
+
+ it('Cartesian3D({...}) returns expected coordinate transformations', () => {
+ expect(Cartesian3D({})).toEqual([['cartesian3D']]);
+ });
});
diff --git a/__tests__/unit/lib/geo.spec.ts b/__tests__/unit/lib/geo.spec.ts
index af82f6335b..f6fb8d039c 100644
--- a/__tests__/unit/lib/geo.spec.ts
+++ b/__tests__/unit/lib/geo.spec.ts
@@ -2,7 +2,7 @@ import { geolib } from '../../../src/lib';
import { GeoView, GeoPath } from '../../../src/composition';
describe('geolib', () => {
- it('geolib() should returns expected geo compoents.', () => {
+ it('geolib() should returns expected geo components.', () => {
expect(geolib()).toEqual({
'composition.geoView': GeoView,
'composition.geoPath': GeoPath,
diff --git a/__tests__/unit/lib/threed.spec.ts b/__tests__/unit/lib/threed.spec.ts
new file mode 100644
index 0000000000..f111745a2a
--- /dev/null
+++ b/__tests__/unit/lib/threed.spec.ts
@@ -0,0 +1,14 @@
+import { threedlib } from '../../../src/lib';
+import { Cartesian3D } from '../../../src/coordinate';
+import { AxisZ } from '../../../src/component';
+import { Point3D } from '../../../src/mark';
+
+describe('threedlib', () => {
+ it('threedlib() should returns expected threed components.', () => {
+ expect(threedlib()).toEqual({
+ 'coordinate.cartesian3D': Cartesian3D,
+ 'component.axisZ': AxisZ,
+ 'mark.point3D': Point3D,
+ });
+ });
+});
diff --git a/site/.dumi/global.ts b/site/.dumi/global.ts
index 9897fc3e26..19c7f87c14 100644
--- a/site/.dumi/global.ts
+++ b/site/.dumi/global.ts
@@ -22,8 +22,11 @@ if (window) {
window as any
).gPluginRoughCanvasRenderer = require('@antv/g-plugin-rough-canvas-renderer');
(window as any).gPluginA11y = require('@antv/g-plugin-a11y');
+ (window as any).gPluginControl = require('@antv/g-plugin-control');
+ (window as any).gPlugin3d = require('@antv/g-plugin-3d');
(window as any).gSvg = require('@antv/g-svg');
(window as any).gWebgl = require('@antv/g-webgl');
+ (window as any).g = require('@antv/g');
(window as any).fecha = require('fecha');
(window as any).React = require('react');
(window as any).dataSet = require('@antv/data-set');
@@ -113,8 +116,10 @@ function extendG2(g2) {
'frame',
'x',
'y',
+ 'z',
'width',
'height',
+ 'depth',
...dimensionKeys('margin'),
...dimensionKeys('padding'),
...dimensionKeys('inset'),
diff --git a/site/.dumirc.ts b/site/.dumirc.ts
index 72b4046e59..776f606dbd 100644
--- a/site/.dumirc.ts
+++ b/site/.dumirc.ts
@@ -215,6 +215,14 @@ export default defineConfig({
},
order: 14,
},
+ {
+ slug: 'spec/threed',
+ title: {
+ zh: '3D 图表 - 3D Charts',
+ en: '3D',
+ },
+ order: 15,
+ },
{
slug: 'spec/theme',
title: {
@@ -329,6 +337,14 @@ export default defineConfig({
},
icon: 'other',
},
+ {
+ slug: 'threed',
+ title: {
+ zh: '3D 可视化',
+ en: '3D Charts',
+ },
+ icon: 'other',
+ },
{
slug: 'interesting',
title: {
diff --git a/site/docs/api/chart.zh.md b/site/docs/api/chart.zh.md
index b79d9abf6a..0603a12dea 100644
--- a/site/docs/api/chart.zh.md
+++ b/site/docs/api/chart.zh.md
@@ -37,6 +37,7 @@ chart.render();
| container | 指定 chart 绘制的 DOM,可以传入 DOM id,也可以直接传入 dom 实例 | `string \| HTMLElement` | |
| width | 图表宽度 | `number` | 640 |
| height | 图表高度 | `number` | 480 |
+| depth | 图表深度,在 3D 图表中使用 | `number` | 0 |
| renderer | 指定渲染引擎,默认使用 canvas。 | | |
| plugins | 指定渲染时使用的插件 ,具体见 [plugin](/api/plugin/rough) | `any[]` | |
| autoFit | 图表是否自适应容器宽高,默认为 `false`,用户需要手动设置 `width` 和 `height`。
当 `autoFit: true` 时,会自动取图表容器的宽高,如果用户设置了 `height`,那么会以用户设置的 `height` 为准。 | `boolean` | false |
@@ -109,7 +110,9 @@ chart.render();
添加 rangeY 图形,具体见 [mark](/spec/mark/range-y)。
### `chart.connector`
+
+
添加 connector 图形,具体见 [mark](/spec/mark/connector)。
### `chart.sankey`
@@ -192,6 +195,10 @@ chart.render();
添加 timingKeyframe 图形,具体见 [composition](/spec/composition/timing-keyframe)。
+### `chart.point3D`
+
+添加 point3D 图形,具体见 [3d](/spec/threed/point-threed)。
+
## 设置属性
### `chart.width`
@@ -215,11 +222,15 @@ chart.render();
设置图形的数据,支持多种数据来源和数据变换,具体见 [data](/spec/data/overview)。
### `chart.encode`
+
+
设置图形每个通道的字段名称,具体见 [encode](/api/encode/overview)。
### `chart.scale`
+
+
设置图形每个通道的比例尺,具体见 [scale](/spec/overview#scale)。
### `chart.legend`
@@ -243,7 +254,9 @@ chart.render();
设置图形的标签,具体见 [label](/spec/label/overview)。
### `chart.style`
+
+
设置图形的样式,具体见 [style](/spec/common/style)。
### `chart.theme`
@@ -251,7 +264,9 @@ chart.render();
设置图形的主题,具体见 [theme](/spec/theme/academy)。
### `chart.labelTransform`
+
+
设置图形的 labelTransform,具体见 [label](/spec/label/overview)
## 渲染图表
diff --git a/site/docs/api/overview.zh.md b/site/docs/api/overview.zh.md
index dce66c2369..1b7c369d8b 100644
--- a/site/docs/api/overview.zh.md
+++ b/site/docs/api/overview.zh.md
@@ -46,6 +46,7 @@ order: 1
- [chart.**timingKeyframe**](/api/chart#charttimingkeyframe) - 添加 timingKeyframe 到该图表。
- [chart.**geoView**](/api/chart#chartgeoview) - 添加 geoView 到该图表。
- [chart.**geoPath**](/api/chart#chartgeopath) - 添加 geoPath 到该图表。
+- [chart.**point3D**](/api/chart#chartpoint3d) - 添加 point3D 标记到该视图。
### 设置属性
diff --git a/site/docs/manual/core/coordinate.zh.md b/site/docs/manual/core/coordinate.zh.md
index fda23b991f..0e9a73a36b 100644
--- a/site/docs/manual/core/coordinate.zh.md
+++ b/site/docs/manual/core/coordinate.zh.md
@@ -327,3 +327,11 @@ chart.area():
return chart.getContainer();
})();
```
+
+## 3D 坐标系
+
+目前我们仅支持 `cartesian3D` 坐标系:
+
+```ts
+chart.coordinate({ type: 'cartesian3D' });
+```
diff --git a/site/docs/manual/core/encode.zh.md b/site/docs/manual/core/encode.zh.md
index 178ebeea9b..e492bf3f57 100644
--- a/site/docs/manual/core/encode.zh.md
+++ b/site/docs/manual/core/encode.zh.md
@@ -244,6 +244,7 @@ chart.encode('y', 'end').encode('y1', 'start');
- **x** - x 位置
- **y** - y 位置
+- **z** - z 位置
- **color** - 颜色,填充色或者边框色,由形状决定
- **opacity** - 透明度,填充透明度或者边框透明度,由样式决定
- **shape** - 形状
diff --git a/site/docs/manual/extra-topics/3d-charts.en.md b/site/docs/manual/extra-topics/3d-charts.en.md
new file mode 100644
index 0000000000..195d7d6f86
--- /dev/null
+++ b/site/docs/manual/extra-topics/3d-charts.en.md
@@ -0,0 +1,6 @@
+---
+title: Use 3D Charts
+order: 11
+---
+
+
diff --git a/site/docs/manual/extra-topics/3d-charts.zh.md b/site/docs/manual/extra-topics/3d-charts.zh.md
new file mode 100644
index 0000000000..92863f0391
--- /dev/null
+++ b/site/docs/manual/extra-topics/3d-charts.zh.md
@@ -0,0 +1,312 @@
+---
+title: 绘制 3D 图表
+order: 11
+---
+
+以 3D 散点图为例,创建图表需要以下步骤:
+
+- 创建 WebGL 渲染器和插件
+- 扩展 threedlib
+- 设置 z 通道、比例尺和坐标轴
+- 在场景中设置相机
+- 添加光源
+- 使用相机交互
+
+我们暂不支持图例。
+
+## 创建 WebGL 渲染器和插件
+
+首先安装依赖:
+
+```bash
+$ npm install @antv/g-webgl @antv/g-plugin-3d @antv/g-plugin-control --save
+```
+
+然后使用 [@antv/g-webgl](https://g.antv.antgroup.com/api/renderer/webgl) 作为渲染器并注册以下两个插件:
+
+- [g-plugin-3d](https://g.antv.antgroup.com/plugins/3d) 提供 3D 场景下的几何、材质和光照
+- [g-plugin-control](https://g.antv.antgroup.com/plugins/control) 提供 3D 场景下的相机交互
+
+```ts
+import { Renderer as WebGLRenderer } from '@antv/g-webgl';
+import { Plugin as ThreeDPlugin, DirectionalLight } from '@antv/g-plugin-3d';
+import { Plugin as ControlPlugin } from '@antv/g-plugin-control';
+
+const renderer = new WebGLRenderer();
+renderer.registerPlugin(new ThreeDPlugin());
+renderer.registerPlugin(new ControlPlugin());
+```
+
+## 扩展 threedlib
+
+由于 3D 相关的功能代码体积巨大,我们将其分离到 [threedlib](/manual/extra-topics/bundle#g2threedlib) 中,在运行时扩展它并自定义 Chart 对象:
+
+```ts
+import { Runtime, corelib, threedlib, extend } from '@antv/g2';
+
+const Chart = extend(Runtime, { ...corelib(), ...threedlib() });
+```
+
+## 设置 z 通道、比例尺和坐标轴
+
+在创建 Chart 时通过 `depth` 指定深度:
+
+```ts
+const chart = new Chart({
+ container: 'container',
+ theme: 'classic',
+ renderer,
+ depth: 400,
+});
+```
+
+我们使用 [point3D](/spec/threed/point-threed) Mark 并选择 cube 作为 shape 进行绘制。
+随后设置 z 通道、比例尺和坐标轴。
+
+```ts
+chart
+ .point3D()
+ .data({
+ type: 'fetch',
+ value:
+ 'https://gw.alipayobjects.com/os/bmw-prod/2c813e2d-2276-40b9-a9af-cf0a0fb7e942.csv',
+ })
+ .encode('x', 'Horsepower')
+ .encode('y', 'Miles_per_Gallon')
+ .encode('z', 'Weight_in_lbs')
+ .encode('size', 'Origin')
+ .encode('color', 'Cylinders')
+ .encode('shape', 'cube')
+ .coordinate({ type: 'cartesian3D' })
+ .scale('x', { nice: true })
+ .scale('y', { nice: true })
+ .scale('z', { nice: true })
+ .legend(false)
+ .axis('x', { gridLineWidth: 2 })
+ .axis('y', { gridLineWidth: 2, titleBillboardRotation: -Math.PI / 2 })
+ .axis('z', { gridLineWidth: 2 });
+```
+
+## 设置相机
+
+在 3D 场景中我们可以使用正交或者透视投影,在首次渲染完成后可以从 Chart 上下文中获取相机。随后可以使用 G 提供的[相机 API](https://g.antv.antgroup.com/api/camera/intro) 完成投影模式、相机类型的设置。在下面的例子中,我们使用了透视投影,
+
+```ts
+chart.render().then(() => {
+ const { canvas } = chart.getContext();
+ const camera = canvas.getCamera(); // 获取相机
+
+ camera.setPerspective(0.1, 5000, 45, 500 / 500);
+ camera.setType(CameraType.ORBITING);
+});
+```
+
+效果如下:
+
+```js | ob { pin: false }
+(() => {
+ const renderer = new gWebgl.Renderer();
+ renderer.registerPlugin(new gPluginControl.Plugin());
+ renderer.registerPlugin(new gPlugin3d.Plugin());
+
+ const Chart = G2.extend(G2.Runtime, { ...G2.corelib(), ...G2.threedlib() });
+
+ // 初始化图表实例
+ const chart = new Chart({
+ theme: 'classic',
+ renderer,
+ width: 500,
+ height: 500,
+ depth: 400,
+ });
+
+ chart
+ .point3D()
+ .data({
+ type: 'fetch',
+ value:
+ 'https://gw.alipayobjects.com/os/bmw-prod/2c813e2d-2276-40b9-a9af-cf0a0fb7e942.csv',
+ })
+ .encode('x', 'Horsepower')
+ .encode('y', 'Miles_per_Gallon')
+ .encode('z', 'Weight_in_lbs')
+ .encode('color', 'Cylinders')
+ .encode('shape', 'cube')
+ .coordinate({ type: 'cartesian3D' })
+ .scale('x', { nice: true })
+ .scale('y', { nice: true })
+ .scale('z', { nice: true })
+ .legend(false)
+ .axis('x', { gridLineWidth: 2 })
+ .axis('y', { gridLineWidth: 2, titleBillboardRotation: -Math.PI / 2 })
+ .axis('z', { gridLineWidth: 2 });
+
+ chart.render().then(() => {
+ const { canvas } = chart.getContext();
+ const camera = canvas.getCamera();
+ camera.setPerspective(0.1, 5000, 45, 500 / 500);
+ camera.setType(g.CameraType.ORBITING);
+
+ // Add a directional light into scene.
+ const light = new gPlugin3d.DirectionalLight({
+ style: {
+ intensity: 3,
+ fill: 'white',
+ direction: [-1, 0, 1],
+ },
+ });
+ canvas.appendChild(light);
+ });
+
+ return chart.getContainer();
+})();
+```
+
+我们还可以让相机固定视点进行一定角度的旋转,这里使用了 [rotate](https://g.antv.antgroup.com/api/camera/action#rotate):
+
+```ts
+camera.rotate(-20, -20, 0);
+```
+
+```js | ob { pin: false }
+(() => {
+ const renderer = new gWebgl.Renderer();
+ renderer.registerPlugin(new gPluginControl.Plugin());
+ renderer.registerPlugin(new gPlugin3d.Plugin());
+
+ const Chart = G2.extend(G2.Runtime, { ...G2.corelib(), ...G2.threedlib() });
+
+ // 初始化图表实例
+ const chart = new Chart({
+ theme: 'classic',
+ renderer,
+ width: 500,
+ height: 500,
+ depth: 400,
+ });
+
+ chart
+ .point3D()
+ .data({
+ type: 'fetch',
+ value:
+ 'https://gw.alipayobjects.com/os/bmw-prod/2c813e2d-2276-40b9-a9af-cf0a0fb7e942.csv',
+ })
+ .encode('x', 'Horsepower')
+ .encode('y', 'Miles_per_Gallon')
+ .encode('z', 'Weight_in_lbs')
+ .encode('color', 'Cylinders')
+ .encode('shape', 'cube')
+ .coordinate({ type: 'cartesian3D' })
+ .scale('x', { nice: true })
+ .scale('y', { nice: true })
+ .scale('z', { nice: true })
+ .legend(false)
+ .axis('x', { gridLineWidth: 2 })
+ .axis('y', { gridLineWidth: 2, titleBillboardRotation: -Math.PI / 2 })
+ .axis('z', { gridLineWidth: 2 });
+
+ chart.render().then(() => {
+ const { canvas } = chart.getContext();
+ const camera = canvas.getCamera();
+ camera.setType(g.CameraType.ORBITING);
+ camera.rotate(-20, -20, 0);
+
+ // Add a directional light into scene.
+ const light = new gPlugin3d.DirectionalLight({
+ style: {
+ intensity: 3,
+ fill: 'white',
+ direction: [-1, 0, 1],
+ },
+ });
+ canvas.appendChild(light);
+ });
+
+ return chart.getContainer();
+})();
+```
+
+## 添加光源
+
+材质需要配合光源呈现出某种“立体感”。这里我们使用 G 提供的[平行光源](https://g.antv.antgroup.com/api/3d/light):
+
+```ts
+import { DirectionalLight } from '@antv/g-plugin-3d';
+
+const light = new DirectionalLight({
+ style: {
+ intensity: 3,
+ fill: 'white',
+ direction: [-1, 0, 1],
+ },
+});
+canvas.appendChild(light);
+```
+
+我们可以通过 `intensity` 增大光源的强度:
+
+```js | ob { pin: false }
+(() => {
+ const renderer = new gWebgl.Renderer();
+ renderer.registerPlugin(new gPluginControl.Plugin());
+ renderer.registerPlugin(new gPlugin3d.Plugin());
+
+ const Chart = G2.extend(G2.Runtime, { ...G2.corelib(), ...G2.threedlib() });
+
+ // 初始化图表实例
+ const chart = new Chart({
+ theme: 'classic',
+ renderer,
+ width: 500,
+ height: 500,
+ depth: 400,
+ });
+
+ chart
+ .point3D()
+ .data({
+ type: 'fetch',
+ value:
+ 'https://gw.alipayobjects.com/os/bmw-prod/2c813e2d-2276-40b9-a9af-cf0a0fb7e942.csv',
+ })
+ .encode('x', 'Horsepower')
+ .encode('y', 'Miles_per_Gallon')
+ .encode('z', 'Weight_in_lbs')
+ .encode('color', 'Cylinders')
+ .encode('shape', 'cube')
+ .coordinate({ type: 'cartesian3D' })
+ .scale('x', { nice: true })
+ .scale('y', { nice: true })
+ .scale('z', { nice: true })
+ .legend(false)
+ .axis('x', { gridLineWidth: 2 })
+ .axis('y', { gridLineWidth: 2, titleBillboardRotation: -Math.PI / 2 })
+ .axis('z', { gridLineWidth: 2 });
+
+ chart.render().then(() => {
+ const { canvas } = chart.getContext();
+ const camera = canvas.getCamera();
+ camera.setPerspective(0.1, 5000, 45, 500 / 500);
+ camera.setType(g.CameraType.ORBITING);
+
+ // Add a directional light into scene.
+ const light = new gPlugin3d.DirectionalLight({
+ style: {
+ intensity: 5,
+ fill: 'white',
+ direction: [0, 0, 1],
+ },
+ });
+ canvas.appendChild(light);
+ });
+
+ return chart.getContainer();
+})();
+```
+
+## 使用相机交互
+
+3D 场景下的交互和 2D 场景有很大的不同,[g-plugin-control](https://g.antv.antgroup.com/plugins/control) 提供了 3D 场景下基于相机的交互。当我们拖拽画布时,会控制相机绕视点进行旋转操作,而鼠标滚轮的缩放会让相机进行 dolly 操作。
+
+需要注意的是缩放操作在正交投影下是没有效果的,但旋转操作依然有效。
diff --git a/site/docs/manual/extra-topics/bundle.zh.md b/site/docs/manual/extra-topics/bundle.zh.md
index 455973b21a..6b9f7e1f54 100644
--- a/site/docs/manual/extra-topics/bundle.zh.md
+++ b/site/docs/manual/extra-topics/bundle.zh.md
@@ -253,7 +253,7 @@ chart.auto(); // Auto Mark
> 开发中,预计 10 月底上线
-返回 3D 分析库,提供 3D 可视化的能力。该 library 不会包含在 [G2.stdlib](#g2stdlib) 里面,同样不能单独使用,需要配合 [G2.corelib](#g2corelib) 使用。
+返回 3D 分析库,提供 3D 可视化的能力。该 library 不会包含在 [G2.stdlib](#g2stdlib) 里面,同样不能单独使用,需要配合 [G2.corelib](#g2corelib) 使用。[示例](/manual/extra-topics/3d-charts)
```js
import { Runtime, extend, threedlib, corelib } from '@antv/g2';
@@ -267,9 +267,10 @@ const Chart = extend(Runtime, {
const chart = new Chart({
theme: 'classic',
renderer: new Renderer(), //使用 webgl 渲染器
+ depth: 400, // 设置深度
});
-chart.interval3d();
+chart.point3D();
```
## 未来工作
diff --git a/site/docs/manual/introduction/why-g2.zh.md b/site/docs/manual/introduction/why-g2.zh.md
index 56b99d08bd..ed80f9cc78 100644
--- a/site/docs/manual/introduction/why-g2.zh.md
+++ b/site/docs/manual/introduction/why-g2.zh.md
@@ -686,7 +686,7 @@ import { Runtime, corelib, extend } from '@antv/g2';
// 基于 corelib 对 Runtime 进行扩展
// 1. 增加类型(如果使用的 TypeScript)
// 2. 增加 Mark
-const Chart = extend(Runtime, corelib);
+const Chart = extend(Runtime, { ...corelib() });
const chart = new Chart({ container: 'container' });
diff --git a/site/docs/spec/coordinate/cartesian3D.en.md b/site/docs/spec/coordinate/cartesian3D.en.md
new file mode 100644
index 0000000000..4865c88f91
--- /dev/null
+++ b/site/docs/spec/coordinate/cartesian3D.en.md
@@ -0,0 +1,6 @@
+---
+title: cartesian3D
+order: 5
+---
+
+
diff --git a/site/docs/spec/coordinate/cartesian3D.zh.md b/site/docs/spec/coordinate/cartesian3D.zh.md
new file mode 100644
index 0000000000..6062942265
--- /dev/null
+++ b/site/docs/spec/coordinate/cartesian3D.zh.md
@@ -0,0 +1,50 @@
+---
+title: cartesian3D
+order: 5
+---
+
+在 2D 笛卡尔坐标系基础上,通过增加 Z 轴扩展而来。[示例](/manual/extra-topics/3d-charts)
+
+## 开始使用
+
+
+
+```js
+import { Runtime, corelib, threedlib, extend } from '@antv/g2';
+
+const Chart = extend(Runtime, { ...corelib(), ...threedlib() });
+
+const chart = new Chart({
+ container: 'container',
+ theme: 'classic',
+ renderer,
+ depth: 400,
+});
+
+chart.coordinate({
+ type: 'cartesian3D',
+});
+
+chart
+ .point3D()
+ .data({
+ type: 'fetch',
+ value:
+ 'https://gw.alipayobjects.com/os/bmw-prod/2c813e2d-2276-40b9-a9af-cf0a0fb7e942.csv',
+ })
+ .encode('x', 'Horsepower')
+ .encode('y', 'Miles_per_Gallon')
+ .encode('z', 'Weight_in_lbs')
+ .encode('size', 'Origin')
+ .encode('color', 'Cylinders')
+ .encode('shape', 'cube')
+ .scale('x', { nice: true })
+ .scale('y', { nice: true })
+ .scale('z', { nice: true })
+ .legend(false)
+ .axis('x', { gridLineWidth: 2 })
+ .axis('y', { gridLineWidth: 2, titleBillboardRotation: -Math.PI / 2 })
+ .axis('z', { gridLineWidth: 2 });
+
+chart.render();
+```
diff --git a/site/docs/spec/threed/pointThreed.en.md b/site/docs/spec/threed/pointThreed.en.md
new file mode 100644
index 0000000000..a7740d28a0
--- /dev/null
+++ b/site/docs/spec/threed/pointThreed.en.md
@@ -0,0 +1,6 @@
+---
+title: point3D
+order: 1
+---
+
+
diff --git a/site/docs/spec/threed/pointThreed.zh.md b/site/docs/spec/threed/pointThreed.zh.md
new file mode 100644
index 0000000000..584a7a0450
--- /dev/null
+++ b/site/docs/spec/threed/pointThreed.zh.md
@@ -0,0 +1,156 @@
+---
+title: point3D
+order: 1
+---
+
+主要用于绘制 3D 散点图,利用点的粒度来分析数据的分布情况。
+
+## 开始使用
+
+首先需要使用 [@antv/g-webgl](https://g.antv.antgroup.com/api/renderer/webgl) 作为渲染器并注册以下两个插件:
+
+- [g-plugin-3d](https://g.antv.antgroup.com/plugins/3d) 提供 3D 场景下的几何、材质和光照
+- [g-plugin-control](https://g.antv.antgroup.com/plugins/control) 提供 3D 场景下的相机交互
+
+然后设置 z 通道、scale 和 z 坐标轴,最后在场景中添加光源。
+
+```js | ob
+(() => {
+ const renderer = new gWebgl.Renderer();
+ renderer.registerPlugin(new gPluginControl.Plugin());
+ renderer.registerPlugin(new gPlugin3d.Plugin());
+
+ const Chart = G2.extend(G2.Runtime, { ...G2.corelib(), ...G2.threedlib() });
+
+ // 初始化图表实例
+ const chart = new Chart({
+ theme: 'classic',
+ renderer,
+ width: 500,
+ height: 500,
+ depth: 400,
+ });
+
+ chart
+ .point3D()
+ .data({
+ type: 'fetch',
+ value:
+ 'https://gw.alipayobjects.com/os/bmw-prod/2c813e2d-2276-40b9-a9af-cf0a0fb7e942.csv',
+ })
+ .encode('x', 'Horsepower')
+ .encode('y', 'Miles_per_Gallon')
+ .encode('z', 'Weight_in_lbs')
+ .encode('color', 'Cylinders')
+ .encode('shape', 'cube')
+ .coordinate({ type: 'cartesian3D' })
+ .scale('x', { nice: true })
+ .scale('y', { nice: true })
+ .scale('z', { nice: true })
+ .legend(false)
+ .axis('x', { gridLineWidth: 2 })
+ .axis('y', { gridLineWidth: 2, titleBillboardRotation: -Math.PI / 2 })
+ .axis('z', { gridLineWidth: 2 });
+
+ chart.render().then(() => {
+ const { canvas } = chart.getContext();
+ const camera = canvas.getCamera();
+ camera.setPerspective(0.1, 5000, 45, 500 / 500);
+ camera.setType(g.CameraType.ORBITING);
+
+ // Add a directional light into scene.
+ const light = new gPlugin3d.DirectionalLight({
+ style: {
+ intensity: 3,
+ fill: 'white',
+ direction: [-1, 0, 1],
+ },
+ });
+ canvas.appendChild(light);
+ });
+
+ return chart.getContainer();
+})();
+```
+
+更多的案例,可以查看[图表示例](/examples)页面。
+
+## 选项
+
+目前 point3D 有以下几个内置 shape 图形:
+
+| 图形 | 描述 | 示例 |
+| ------ | ---------- | ---- |
+| cube | 绘制立方体 | |
+| sphere | 绘制球体 | |
+
+使用球体效果如下:
+
+```js | ob { pin: false }
+(() => {
+ const renderer = new gWebgl.Renderer();
+ renderer.registerPlugin(new gPluginControl.Plugin());
+ renderer.registerPlugin(new gPlugin3d.Plugin());
+
+ const Chart = G2.extend(G2.Runtime, { ...G2.corelib(), ...G2.threedlib() });
+
+ // 初始化图表实例
+ const chart = new Chart({
+ theme: 'classic',
+ renderer,
+ width: 500,
+ height: 500,
+ depth: 400,
+ });
+
+ chart
+ .point3D()
+ .data({
+ type: 'fetch',
+ value:
+ 'https://gw.alipayobjects.com/os/bmw-prod/2c813e2d-2276-40b9-a9af-cf0a0fb7e942.csv',
+ })
+ .encode('x', 'Horsepower')
+ .encode('y', 'Miles_per_Gallon')
+ .encode('z', 'Weight_in_lbs')
+ .encode('color', 'Cylinders')
+ .encode('shape', 'sphere')
+ .coordinate({ type: 'cartesian3D' })
+ .scale('x', { nice: true })
+ .scale('y', { nice: true })
+ .scale('z', { nice: true })
+ .legend(false)
+ .axis('x', { gridLineWidth: 2 })
+ .axis('y', { gridLineWidth: 2, titleBillboardRotation: -Math.PI / 2 })
+ .axis('z', { gridLineWidth: 2 });
+
+ chart.render().then(() => {
+ const { canvas } = chart.getContext();
+ const camera = canvas.getCamera();
+ camera.setPerspective(0.1, 5000, 45, 500 / 500);
+ camera.setType(g.CameraType.ORBITING);
+
+ // Add a directional light into scene.
+ const light = new gPlugin3d.DirectionalLight({
+ style: {
+ intensity: 3,
+ fill: 'white',
+ direction: [-1, 0, 1],
+ },
+ });
+ canvas.appendChild(light);
+ });
+
+ return chart.getContainer();
+})();
+```
+
+### cube
+
+| 属性 | 描述 | 类型 | 默认值 |
+| ------- | --------------------------------------------- | ------------------------------ | --------- |
+| fill | 图形的填充色 | `string` \| `Function` | - |
+| opacity | 图形的整体透明度 | `number` \| `Function` | - |
+| cursor | 鼠标样式。同 css 的鼠标样式,默认 'default'。 | `string` \| `Function` | 'default' |
+
+其他的 point3D 图形配置项和 `cube` 一致。
diff --git a/site/examples/3d/scatter/demo/meta.json b/site/examples/3d/scatter/demo/meta.json
new file mode 100644
index 0000000000..42dd2cc544
--- /dev/null
+++ b/site/examples/3d/scatter/demo/meta.json
@@ -0,0 +1,32 @@
+{
+ "title": {
+ "zh": "中文分类",
+ "en": "Category"
+ },
+ "demos": [
+ {
+ "filename": "orthographic-projection.ts",
+ "title": {
+ "zh": "正交投影",
+ "en": "Orthographic projection"
+ },
+ "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*7MdMQY-QksEAAAAAAAAAAAAADmJ7AQ/original"
+ },
+ {
+ "filename": "perspective-projection.ts",
+ "title": {
+ "zh": "透视投影",
+ "en": "Perspective projection"
+ },
+ "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*KNCUQqzw2JsAAAAAAAAAAAAADmJ7AQ/original"
+ },
+ {
+ "filename": "sphere-shape.ts",
+ "title": {
+ "zh": "球体",
+ "en": "Sphere"
+ },
+ "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*KNCUQqzw2JsAAAAAAAAAAAAADmJ7AQ/original"
+ }
+ ]
+}
diff --git a/site/examples/3d/scatter/demo/orthographic-projection.ts b/site/examples/3d/scatter/demo/orthographic-projection.ts
new file mode 100644
index 0000000000..2583d096e4
--- /dev/null
+++ b/site/examples/3d/scatter/demo/orthographic-projection.ts
@@ -0,0 +1,56 @@
+import { CameraType } from '@antv/g';
+import { Renderer as WebGLRenderer } from '@antv/g-webgl';
+import { Plugin as ThreeDPlugin, DirectionalLight } from '@antv/g-plugin-3d';
+import { Plugin as ControlPlugin } from '@antv/g-plugin-control';
+import { Runtime, corelib, threedlib, extend } from '@antv/g2';
+
+// Create a WebGL renderer.
+const renderer = new WebGLRenderer();
+renderer.registerPlugin(new ThreeDPlugin());
+renderer.registerPlugin(new ControlPlugin());
+
+// Customize our own Chart with threedlib.
+const Chart = extend(Runtime, { ...corelib(), ...threedlib() });
+const chart = new Chart({
+ container: 'container',
+ theme: 'classic',
+ renderer,
+ depth: 400, // Define the depth of chart.
+});
+
+chart
+ .point3D()
+ .data({
+ type: 'fetch',
+ value:
+ 'https://gw.alipayobjects.com/os/bmw-prod/2c813e2d-2276-40b9-a9af-cf0a0fb7e942.csv',
+ })
+ .encode('x', 'Horsepower')
+ .encode('y', 'Miles_per_Gallon')
+ .encode('z', 'Weight_in_lbs')
+ .encode('color', 'Cylinders')
+ .encode('shape', 'cube')
+ .coordinate({ type: 'cartesian3D' })
+ .scale('x', { nice: true })
+ .scale('y', { nice: true })
+ .scale('z', { nice: true })
+ .legend(false)
+ .axis('x', { gridLineWidth: 2 })
+ .axis('y', { gridLineWidth: 2, titleBillboardRotation: -Math.PI / 2 })
+ .axis('z', { gridLineWidth: 2 });
+
+chart.render().then(() => {
+ const { canvas } = chart.getContext();
+ const camera = canvas.getCamera();
+ camera.setType(CameraType.ORBITING);
+
+ // Add a directional light into scene.
+ const light = new DirectionalLight({
+ style: {
+ intensity: 3,
+ fill: 'white',
+ direction: [-1, 0, 1],
+ },
+ });
+ canvas.appendChild(light);
+});
diff --git a/site/examples/3d/scatter/demo/perspective-projection.ts b/site/examples/3d/scatter/demo/perspective-projection.ts
new file mode 100644
index 0000000000..b858addcaa
--- /dev/null
+++ b/site/examples/3d/scatter/demo/perspective-projection.ts
@@ -0,0 +1,58 @@
+import { CameraType } from '@antv/g';
+import { Renderer as WebGLRenderer } from '@antv/g-webgl';
+import { Plugin as ThreeDPlugin, DirectionalLight } from '@antv/g-plugin-3d';
+import { Plugin as ControlPlugin } from '@antv/g-plugin-control';
+import { Runtime, corelib, threedlib, extend } from '@antv/g2';
+
+// Create a WebGL renderer.
+const renderer = new WebGLRenderer();
+renderer.registerPlugin(new ThreeDPlugin());
+renderer.registerPlugin(new ControlPlugin());
+
+// Customize our own Chart with threedlib.
+const Chart = extend(Runtime, { ...corelib(), ...threedlib() });
+const chart = new Chart({
+ container: 'container',
+ theme: 'classic',
+ renderer,
+ depth: 400, // Define the depth of chart.
+});
+
+chart
+ .point3D()
+ .data({
+ type: 'fetch',
+ value:
+ 'https://gw.alipayobjects.com/os/bmw-prod/2c813e2d-2276-40b9-a9af-cf0a0fb7e942.csv',
+ })
+ .encode('x', 'Horsepower')
+ .encode('y', 'Miles_per_Gallon')
+ .encode('z', 'Weight_in_lbs')
+ .encode('color', 'Cylinders')
+ .encode('shape', 'cube')
+ .coordinate({ type: 'cartesian3D' })
+ .scale('x', { nice: true })
+ .scale('y', { nice: true })
+ .scale('z', { nice: true })
+ .legend(false)
+ .axis('x', { gridLineWidth: 2 })
+ .axis('y', { gridLineWidth: 2, titleBillboardRotation: -Math.PI / 2 })
+ .axis('z', { gridLineWidth: 2 });
+
+chart.render().then(() => {
+ const { canvas } = chart.getContext();
+ const camera = canvas.getCamera();
+ // Use perspective projection mode.
+ camera.setPerspective(0.1, 5000, 45, 640 / 480);
+ camera.setType(CameraType.ORBITING);
+
+ // Add a directional light into scene.
+ const light = new DirectionalLight({
+ style: {
+ intensity: 3,
+ fill: 'white',
+ direction: [-1, 0, 1],
+ },
+ });
+ canvas.appendChild(light);
+});
diff --git a/site/examples/3d/scatter/demo/sphere-shape.ts b/site/examples/3d/scatter/demo/sphere-shape.ts
new file mode 100644
index 0000000000..f7d68853ef
--- /dev/null
+++ b/site/examples/3d/scatter/demo/sphere-shape.ts
@@ -0,0 +1,58 @@
+import { CameraType } from '@antv/g';
+import { Renderer as WebGLRenderer } from '@antv/g-webgl';
+import { Plugin as ThreeDPlugin, DirectionalLight } from '@antv/g-plugin-3d';
+import { Plugin as ControlPlugin } from '@antv/g-plugin-control';
+import { Runtime, corelib, threedlib, extend } from '@antv/g2';
+
+// Create a WebGL renderer.
+const renderer = new WebGLRenderer();
+renderer.registerPlugin(new ThreeDPlugin());
+renderer.registerPlugin(new ControlPlugin());
+
+// Customize our own Chart with threedlib.
+const Chart = extend(Runtime, { ...corelib(), ...threedlib() });
+const chart = new Chart({
+ container: 'container',
+ theme: 'classic',
+ renderer,
+ depth: 400, // Define the depth of chart.
+});
+
+chart
+ .point3D()
+ .data({
+ type: 'fetch',
+ value:
+ 'https://gw.alipayobjects.com/os/bmw-prod/2c813e2d-2276-40b9-a9af-cf0a0fb7e942.csv',
+ })
+ .encode('x', 'Horsepower')
+ .encode('y', 'Miles_per_Gallon')
+ .encode('z', 'Weight_in_lbs')
+ .encode('size', 'Origin')
+ .encode('color', 'Cylinders')
+ .encode('shape', 'sphere')
+ .coordinate({ type: 'cartesian3D' })
+ .scale('x', { nice: true })
+ .scale('y', { nice: true })
+ .scale('z', { nice: true })
+ .legend(false)
+ .axis('x', { gridLineWidth: 2 })
+ .axis('y', { gridLineWidth: 2, titleBillboardRotation: -Math.PI / 2 })
+ .axis('z', { gridLineWidth: 2 });
+
+chart.render().then(() => {
+ const { canvas } = chart.getContext();
+ const camera = canvas.getCamera();
+ camera.setPerspective(0.1, 5000, 45, 640 / 480);
+ camera.setType(CameraType.ORBITING);
+
+ // Add a directional light into scene.
+ const light = new DirectionalLight({
+ style: {
+ intensity: 3,
+ fill: 'white',
+ direction: [-1, 0, 1],
+ },
+ });
+ canvas.appendChild(light);
+});
diff --git a/site/examples/3d/scatter/index.en.md b/site/examples/3d/scatter/index.en.md
new file mode 100644
index 0000000000..f0033100eb
--- /dev/null
+++ b/site/examples/3d/scatter/index.en.md
@@ -0,0 +1,4 @@
+---
+title: 3D Scatter Chart
+order: 1
+---
diff --git a/site/examples/3d/scatter/index.zh.md b/site/examples/3d/scatter/index.zh.md
new file mode 100644
index 0000000000..3bb0cf62d9
--- /dev/null
+++ b/site/examples/3d/scatter/index.zh.md
@@ -0,0 +1,4 @@
+---
+title: 3D 散点图
+order: 1
+---
diff --git a/site/package.json b/site/package.json
index 62c5477054..04e2804f85 100644
--- a/site/package.json
+++ b/site/package.json
@@ -13,6 +13,8 @@
"@antv/g-lottie-player": "^0.2.7",
"@antv/g-pattern": "^1.2.7",
"@antv/g-plugin-a11y": "^0.6.7",
+ "@antv/g-plugin-3d": "^1.9.5",
+ "@antv/g-plugin-control": "^1.9.5",
"@antv/g-plugin-rough-canvas-renderer": "^1.9.7",
"@antv/g-svg": "^1.10.7",
"@antv/g-webgl": "^1.9.8",
diff --git a/src/api/runtime.ts b/src/api/runtime.ts
index 3ecd3f4eb7..1f6e4de39c 100644
--- a/src/api/runtime.ts
+++ b/src/api/runtime.ts
@@ -301,11 +301,11 @@ export class Runtime extends CompositionNode {
private _computedOptions() {
const options = this.options();
const { key = G2_CHART_KEY } = options;
- const { width, height } = sizeOf(options, this._container);
+ const { width, height, depth } = sizeOf(options, this._container);
this._width = width;
this._height = height;
this._key = key;
- return { key: this._key, ...options, width, height };
+ return { key: this._key, ...options, width, height, depth };
}
// Create canvas if it does not exist.
diff --git a/src/api/utils.ts b/src/api/utils.ts
index b658ef5373..262c02ef39 100644
--- a/src/api/utils.ts
+++ b/src/api/utils.ts
@@ -8,6 +8,7 @@ import { Node } from './node';
export const VIEW_KEYS = [
'width',
'height',
+ 'depth',
'padding',
'paddingLeft',
'paddingRight',
@@ -63,8 +64,8 @@ export function valueOf(node: Node): Record {
export function sizeOf(options, container) {
const { autoFit } = options;
if (autoFit) return getContainerSize(container);
- const { width = 640, height = 480 } = options;
- return { width, height };
+ const { width = 640, height = 480, depth = 0 } = options;
+ return { width, height, depth };
}
export function optionsOf(node: Node): Record {
diff --git a/src/component/axis.ts b/src/component/axis.ts
index 72f343b6f2..fcb697a6bb 100644
--- a/src/component/axis.ts
+++ b/src/component/axis.ts
@@ -11,7 +11,9 @@ import {
GuideComponentComponent as GCC,
GuideComponentOrientation as GCO,
GuideComponentPosition as GCP,
+ GuideComponentPlane,
Scale,
+ Vector3,
} from '../runtime';
import {
angleOf,
@@ -24,11 +26,11 @@ import {
radiusOf,
} from '../utils/coordinate';
import { capitalizeFirst } from '../utils/helper';
-import { isOrdinalScale } from '../utils/scale';
import { adaptor, isVertical, titleContent } from './utils';
export type AxisOptions = {
position?: GCP;
+ plane?: GuideComponentPlane;
zIndex?: number;
title?: string | string[];
direction?: 'left' | 'center' | 'right';
@@ -50,13 +52,31 @@ export type AxisOptions = {
grid: any;
// options won't be overridden
important: Record;
+ /**
+ * Rotation origin.
+ */
+ origin?: Vector3;
+ /**
+ * EulerAngles of rotation.
+ */
+ eulerAngles?: Vector3;
[key: string]: any;
};
-function sizeOf(coordinate: Coordinate): [number, number] {
+export function rotateAxis(axis: DisplayObject, options: AxisOptions) {
+ const { eulerAngles, origin } = options;
+ if (origin) {
+ axis.setOrigin(origin);
+ }
+ if (eulerAngles) {
+ axis.rotate(eulerAngles[0], eulerAngles[1], eulerAngles[2]);
+ }
+}
+
+function sizeOf(coordinate: Coordinate): [number, number, number] {
// @ts-ignore
- const { innerWidth, innerHeight } = coordinate.getOptions();
- return [innerWidth, innerHeight];
+ const { innerWidth, innerHeight, depth } = coordinate.getOptions();
+ return [innerWidth, innerHeight, depth];
}
function createFisheye(position, coordinate) {
@@ -193,10 +213,23 @@ function getData(
});
}
-function inferGridLength(position: GCP, coordinate: Coordinate) {
- const [width, height] = sizeOf(coordinate);
- if (position.includes('bottom') || position.includes('top')) return height;
- return width;
+function inferGridLength(
+ position: GCP,
+ coordinate: Coordinate,
+ plane: GuideComponentPlane = 'xy',
+) {
+ const [width, height, depth] = sizeOf(coordinate);
+
+ if (plane === 'xy') {
+ if (position.includes('bottom') || position.includes('top')) return height;
+ return width;
+ } else if (plane === 'xz') {
+ if (position.includes('bottom') || position.includes('top')) return depth;
+ return width;
+ } else {
+ if (position.includes('bottom') || position.includes('top')) return height;
+ return depth;
+ }
}
function inferLabelOverlap(transform = [], style: Record) {
@@ -280,6 +313,20 @@ function inferGrid(value: boolean, coordinate: Coordinate, scale: Scale) {
return value === undefined ? !!scale.getTicks : value;
}
+function infer3DAxisLinearOverrideStyle(coordinate: Coordinate) {
+ // @ts-ignore
+ const { depth } = coordinate.getOptions();
+ return depth
+ ? {
+ tickIsBillboard: true,
+ lineIsBillboard: true,
+ labelIsBillboard: true,
+ titleIsBillboard: true,
+ gridIsBillboard: true,
+ }
+ : {};
+}
+
function inferAxisLinearOverrideStyle(
position: GCP,
orientation: GCO,
@@ -492,6 +539,7 @@ const LinearAxisComponent: GCC = (options) => {
labelFormatter,
order,
orientation,
+ actualPosition,
position,
size,
style = {},
@@ -523,7 +571,12 @@ const LinearAxisComponent: GCC = (options) => {
...userDefinitions,
};
- const gridLength = inferGridLength(position, coordinate);
+ const gridLength = inferGridLength(
+ actualPosition || position,
+ coordinate,
+ options.plane,
+ );
+
const overrideStyle = inferAxisLinearOverrideStyle(
position,
orientation,
@@ -531,6 +584,8 @@ const LinearAxisComponent: GCC = (options) => {
coordinate,
);
+ const threeDOverrideStyle = infer3DAxisLinearOverrideStyle(coordinate);
+
const data = getData(
scale,
domain,
@@ -568,6 +623,7 @@ const LinearAxisComponent: GCC = (options) => {
indexBBox,
...(!internalAxisStyle.line ? { lineOpacity: 0 } : null),
...overrideStyle,
+ ...threeDOverrideStyle,
...important,
};
diff --git a/src/component/axisX.ts b/src/component/axisX.ts
index 9ae981aef2..139eb0b09f 100644
--- a/src/component/axisX.ts
+++ b/src/component/axisX.ts
@@ -1,5 +1,5 @@
import { GuideComponentComponent as GCC } from '../runtime';
-import { LinearAxis, AxisOptions } from './axis';
+import { AxisOptions, LinearAxis, rotateAxis } from './axis';
export type AxisXOptions = AxisOptions;
@@ -7,9 +7,14 @@ export type AxisXOptions = AxisOptions;
* LinearAxis component bind to x scale.
*/
export const AxisX: GCC = (options) => {
- return (...args) =>
+ return (...args) => {
// empirical value for crossPadding
- LinearAxis(Object.assign({}, { crossPadding: 50 }, options))(...args);
+ const axisX = LinearAxis(Object.assign({}, { crossPadding: 50 }, options))(
+ ...args,
+ );
+ rotateAxis(axisX, options);
+ return axisX;
+ };
};
AxisX.props = {
diff --git a/src/component/axisY.ts b/src/component/axisY.ts
index eae555c6af..d6b147d9d1 100644
--- a/src/component/axisY.ts
+++ b/src/component/axisY.ts
@@ -1,5 +1,5 @@
import { GuideComponentComponent as GCC } from '../runtime';
-import { AxisOptions, LinearAxis } from './axis';
+import { AxisOptions, LinearAxis, rotateAxis } from './axis';
export type AxisYOptions = AxisOptions;
@@ -7,8 +7,13 @@ export type AxisYOptions = AxisOptions;
* LinearAxis component bind to y scale.
*/
export const AxisY: GCC = (options) => {
- return (...args) =>
- LinearAxis(Object.assign({}, { crossPadding: 10 }, options))(...args);
+ return (...args) => {
+ const axisY = LinearAxis(Object.assign({}, { crossPadding: 10 }, options))(
+ ...args,
+ );
+ rotateAxis(axisY, options);
+ return axisY;
+ };
};
AxisY.props = {
diff --git a/src/component/axisZ.ts b/src/component/axisZ.ts
new file mode 100644
index 0000000000..9faf90fa58
--- /dev/null
+++ b/src/component/axisZ.ts
@@ -0,0 +1,25 @@
+import { GuideComponentComponent as GCC } from '../runtime';
+import { LinearAxis, AxisOptions, rotateAxis } from './axis';
+
+export type AxisXOptions = AxisOptions;
+
+/**
+ * LinearAxis component bind to z scale.
+ */
+export const AxisZ: GCC = (options) => {
+ return (...args) => {
+ const axisZ = LinearAxis(Object.assign({}, { crossPadding: 10 }, options))(
+ ...args,
+ );
+ rotateAxis(axisZ, options);
+ return axisZ;
+ };
+};
+
+AxisZ.props = {
+ ...LinearAxis.props,
+ defaultPosition: 'bottom',
+ defaultPlane: 'yz',
+};
+
+export function axisZConfig() {}
diff --git a/src/component/index.ts b/src/component/index.ts
index 1eaae0e5c9..302aac30bd 100644
--- a/src/component/index.ts
+++ b/src/component/index.ts
@@ -1,6 +1,7 @@
export { LinearAxis as AxisLinear, ArcAxis as AxisArc } from './axis';
export { AxisX } from './axisX';
export { AxisY } from './axisY';
+export { AxisZ } from './axisZ';
export { AxisRadar } from './axisRadar';
export { LegendCategory } from './legendCategory';
export { LegendContinuous } from './legendContinuous';
diff --git a/src/composition/mark.ts b/src/composition/mark.ts
index 064df51451..42b26e08d2 100644
--- a/src/composition/mark.ts
+++ b/src/composition/mark.ts
@@ -11,6 +11,7 @@ export const Mark: CC = ({
const {
width,
height,
+ depth,
paddingLeft,
paddingRight,
paddingTop,
@@ -33,6 +34,7 @@ export const Mark: CC = ({
interaction,
x,
y,
+ z,
key,
frame,
labelTransform,
@@ -48,9 +50,11 @@ export const Mark: CC = ({
type: 'standardView',
x,
y,
+ z,
key,
width,
height,
+ depth,
padding,
paddingLeft,
paddingRight,
diff --git a/src/coordinate/cartesian3D.ts b/src/coordinate/cartesian3D.ts
new file mode 100644
index 0000000000..8fac654e2f
--- /dev/null
+++ b/src/coordinate/cartesian3D.ts
@@ -0,0 +1,11 @@
+import { Coordinate3DComponent as CC } from '../runtime';
+import { Cartesian3DCoordinate } from '../spec';
+
+export type Cartesian3DOptions = Cartesian3DCoordinate;
+
+/**
+ * Default coordinate3D transformation for all charts.
+ */
+export const Cartesian3D: CC = () => [['cartesian3D']];
+
+Cartesian3D.props = {};
diff --git a/src/coordinate/index.ts b/src/coordinate/index.ts
index 311e64a569..ea08ef4587 100644
--- a/src/coordinate/index.ts
+++ b/src/coordinate/index.ts
@@ -1,4 +1,5 @@
export { Cartesian } from './cartesian';
+export { Cartesian3D } from './cartesian3D';
export { Polar, getPolarOptions } from './polar';
export { Helix } from './helix';
export { Transpose } from './transpose';
@@ -9,6 +10,7 @@ export { Fisheye } from './fisheye';
export { Radar } from './radar';
export type { CartesianOptions } from './cartesian';
+export type { Cartesian3DOptions } from './cartesian3D';
export type { PolarOptions } from './polar';
export type { HelixOptions } from './helix';
export type { TransposeOptions } from './transpose';
diff --git a/src/index.ts b/src/index.ts
index fe46b86196..33eae8c927 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -15,7 +15,7 @@ export {
AREA_CLASS_NAME,
} from './runtime';
-export { corelib, stdlib, litelib } from './lib';
+export { corelib, stdlib, litelib, threedlib } from './lib';
export * from './mark';
diff --git a/src/lib/index.ts b/src/lib/index.ts
index 6356b89857..e254b20b6c 100644
--- a/src/lib/index.ts
+++ b/src/lib/index.ts
@@ -2,5 +2,6 @@ export { corelib } from './core';
export { geolib } from './geo';
export { graphlib } from './graph';
export { plotlib } from './plot';
+export { threedlib } from './threed';
export { stdlib } from './std';
export { litelib } from './lite';
diff --git a/src/lib/threed.ts b/src/lib/threed.ts
new file mode 100644
index 0000000000..af29661450
--- /dev/null
+++ b/src/lib/threed.ts
@@ -0,0 +1,11 @@
+import { Cartesian3D } from '../coordinate';
+import { AxisZ } from '../component';
+import { Point3D } from '../mark';
+
+export function threedlib() {
+ return {
+ 'coordinate.cartesian3D': Cartesian3D,
+ 'component.axisZ': AxisZ,
+ 'mark.point3D': Point3D,
+ } as const;
+}
diff --git a/src/mark/index.ts b/src/mark/index.ts
index ae5a12949b..ee029a3e60 100644
--- a/src/mark/index.ts
+++ b/src/mark/index.ts
@@ -2,6 +2,7 @@ export { Interval } from './interval';
export { Rect } from './rect';
export { Line } from './line';
export { Point } from './point';
+export { Point3D } from './point3D';
export { Text } from './text';
export { Cell } from './cell';
export { Area } from './area';
diff --git a/src/mark/point3D.ts b/src/mark/point3D.ts
new file mode 100644
index 0000000000..878aaf2ccf
--- /dev/null
+++ b/src/mark/point3D.ts
@@ -0,0 +1,90 @@
+import { Coordinate3D } from '@antv/coord';
+import { MarkComponent as MC, Vector3 } from '../runtime';
+import { PointMark } from '../spec';
+import { MaybeZeroX, MaybeZeroY, MaybeZeroZ, MaybeSize } from '../transform';
+import { Sphere, Cube } from '../shape';
+import {
+ baseGeometryChannels,
+ basePostInference,
+ basePreInference,
+ tooltip3d,
+} from './utils';
+
+export type PointOptions = Omit;
+
+/**
+ * Convert value for each channel to point shapes.
+ * Calc the bbox of each point based on x, y and r.
+ * This is for allowing their radius can be affected by coordinate(e.g. fisheye).
+ */
+export const Point3D: MC = (options) => {
+ return (index, _, value, coordinate) => {
+ const { x: X, y: Y, z: Z, size: S, dx: DX, dy: DY, dz: DZ } = value;
+ const [width, height, depth] = (
+ coordinate as unknown as Coordinate3D
+ ).getSize();
+ const xyz: (i: number) => Vector3 = (i) => {
+ const dx = +(DX?.[i] || 0);
+ const dy = +(DY?.[i] || 0);
+ const dz = +(DZ?.[i] || 0);
+ const x = +X[i];
+ const y = +Y[i];
+ const z = +Z[i];
+ const cx = x + dx;
+ const cy = y + dy;
+ const cz = z + dz;
+ return [cx, cy, cz];
+ };
+ const P = S
+ ? Array.from(index, (i) => {
+ const [cx, cy, cz] = xyz(i);
+ const r = +S[i];
+ const a = r / width;
+ const b = r / height;
+ const c = r / depth;
+ const p1: Vector3 = [cx - a, cy - b, cz - c];
+ const p2: Vector3 = [cx + a, cy + b, cz + c];
+ return [
+ (coordinate as unknown as Coordinate3D).map([...p1, cz]),
+ (coordinate as unknown as Coordinate3D).map([...p2, cz]),
+ ] as Vector3[];
+ })
+ : Array.from(index, (i) => {
+ const [cx, cy, cz] = xyz(i);
+ return [
+ (coordinate as unknown as Coordinate3D).map([cx, cy, cz]),
+ ] as Vector3[];
+ });
+ return [index, P];
+ };
+};
+
+const shape = {
+ sphere: Sphere,
+ cube: Cube,
+};
+
+Point3D.props = {
+ defaultShape: 'sphere',
+ defaultLabelShape: 'label',
+ composite: false,
+ shape,
+ channels: [
+ ...baseGeometryChannels({ shapes: Object.keys(shape) }),
+ { name: 'x', required: true },
+ { name: 'y', required: true },
+ { name: 'z', required: true },
+ { name: 'series', scale: 'band' },
+ { name: 'size', quantitative: 'sqrt' },
+ { name: 'dx', scale: 'identity' },
+ { name: 'dy', scale: 'identity' },
+ { name: 'dz', scale: 'identity' },
+ ],
+ preInference: [
+ ...basePreInference(),
+ { type: MaybeZeroX },
+ { type: MaybeZeroY },
+ { type: MaybeZeroZ },
+ ],
+ postInference: [...basePostInference(), { type: MaybeSize }, ...tooltip3d()],
+};
diff --git a/src/mark/utils.ts b/src/mark/utils.ts
index 6b3f92f6af..9efdb1c238 100644
--- a/src/mark/utils.ts
+++ b/src/mark/utils.ts
@@ -30,6 +30,13 @@ export function baseGeometryChannels(options: ChannelOptions = {}): Channel[] {
return [...baseChannels(options), { name: 'title', scale: 'identity' }];
}
+export function tooltip3d() {
+ return [
+ { type: MaybeTitle, channel: 'color' },
+ { type: MaybeTooltip, channel: ['x', 'y', 'z'] },
+ ];
+}
+
export function tooltip2d() {
return [
{ type: MaybeTitle, channel: 'color' },
diff --git a/src/runtime/component.ts b/src/runtime/component.ts
index 9437bdc427..23364ef1b9 100644
--- a/src/runtime/component.ts
+++ b/src/runtime/component.ts
@@ -102,6 +102,7 @@ export function inferComponent(
const { props } = createGuideComponent(type);
const {
defaultPosition,
+ defaultPlane = 'xy',
defaultOrientation,
defaultSize,
defaultOrder,
@@ -148,6 +149,7 @@ export function inferComponent(
defaultSize,
length,
position,
+ plane: defaultPlane,
orientation,
padding,
order,
@@ -363,6 +365,10 @@ function inferAxisComponentType(
if (isRadial(coordinates)) return ['axisArc', [scale]];
return [isTranspose(coordinates) ? 'axisX' : 'axisY', [scale]];
}
+ // Only support linear axis for z.
+ if (name.startsWith('z')) {
+ return ['axisZ', [scale]];
+ }
if (name.startsWith('position')) {
if (isRadar(coordinates)) return ['axisRadar', [scale]];
if (!isPolar(coordinates)) return ['axisY', [scale]];
diff --git a/src/runtime/coordinate.ts b/src/runtime/coordinate.ts
index e8b2c09ee2..c2664af7a3 100644
--- a/src/runtime/coordinate.ts
+++ b/src/runtime/coordinate.ts
@@ -1,4 +1,4 @@
-import { Coordinate } from '@antv/coord';
+import { Coordinate, Coordinate3D } from '@antv/coord';
import { G2View, G2CoordinateOptions, G2Library } from './types/options';
import { CoordinateComponent, CoordinateTransform } from './types/component';
import { useLibrary } from './library';
@@ -26,7 +26,9 @@ export function createCoordinate(
} = layout;
const { coordinates: partialTransform = [] } = partialOptions;
const transform = inferCoordinate(partialTransform);
- const coordinate = new Coordinate({
+
+ const isCartesian3D = transform[0].type === 'cartesian3D';
+ const options = {
// @todo Find a better solution.
// Store more layout information for component.
...layout,
@@ -35,8 +37,13 @@ export function createCoordinate(
width: innerWidth - insetLeft - insetRight,
height: innerHeight - insetBottom - insetTop,
transformations: transform.map(useCoordinate).flat(),
- });
- return coordinate;
+ };
+
+ const coordinate = isCartesian3D
+ ? // @ts-ignore
+ new Coordinate3D(options)
+ : new Coordinate(options);
+ return coordinate as Coordinate;
}
export function coordinate2Transform(node: G2View, library: G2Library): G2View {
@@ -112,6 +119,9 @@ export function isReflectY(coordinates: G2CoordinateOptions[]) {
function inferCoordinate(
coordinates: G2CoordinateOptions[],
): G2CoordinateOptions[] {
- if (coordinates.find((d) => d.type === 'cartesian')) return coordinates;
+ if (
+ coordinates.find((d) => d.type === 'cartesian' || d.type === 'cartesian3D')
+ )
+ return coordinates;
return [...coordinates, { type: 'cartesian' }];
}
diff --git a/src/runtime/layout.ts b/src/runtime/layout.ts
index 8d98b8b04d..5e414c1d5c 100644
--- a/src/runtime/layout.ts
+++ b/src/runtime/layout.ts
@@ -12,6 +12,7 @@ import {
Section,
SectionArea,
G2Theme,
+ GuideComponentPlane,
} from './types/common';
import {
computeComponentSize,
@@ -24,6 +25,45 @@ import {
import { G2GuideComponentOptions, G2Library, G2View } from './types/options';
import { isPolar as isPolarOptions } from './coordinate';
+export function processAxisZ(components: G2GuideComponentOptions[]) {
+ const axisZ = components.find(({ type }) => type === 'axisZ');
+ if (axisZ) {
+ const axisX = components.find(({ type }) => type === 'axisX');
+ axisX.plane = 'xy';
+ const axisY = components.find(({ type }) => type === 'axisY');
+ axisY.plane = 'xy';
+ axisZ.plane = 'yz';
+ axisZ.origin = [axisX.bbox.x, axisX.bbox.y, 0];
+ axisZ.eulerAngles = [0, -90, 0];
+ axisZ.bbox.x = axisX.bbox.x;
+ axisZ.bbox.y = axisX.bbox.y;
+ components.push({
+ ...axisX,
+ plane: 'xz',
+ showLabel: false,
+ showTitle: false,
+ origin: [axisX.bbox.x, axisX.bbox.y, 0],
+ eulerAngles: [-90, 0, 0],
+ });
+ components.push({
+ ...axisY,
+ plane: 'yz',
+ showLabel: false,
+ showTitle: false,
+ origin: [axisY.bbox.x + axisY.bbox.width, axisY.bbox.y, 0],
+ eulerAngles: [0, -90, 0],
+ });
+ components.push({
+ ...axisZ,
+ plane: 'xz',
+ actualPosition: 'left',
+ showLabel: false,
+ showTitle: false,
+ eulerAngles: [90, -90, 0],
+ });
+ }
+}
+
export function computeLayout(
components: G2GuideComponentOptions[],
options: G2View,
@@ -33,8 +73,10 @@ export function computeLayout(
const {
width,
height,
+ depth,
x = 0,
y = 0,
+ z = 0,
inset = 0,
insetLeft = inset,
insetTop = inset,
@@ -109,6 +151,7 @@ export function computeLayout(
return {
width,
height,
+ depth,
insetLeft,
insetTop,
insetBottom,
@@ -125,6 +168,7 @@ export function computeLayout(
marginRight,
x,
y,
+ z,
};
}
@@ -314,7 +358,11 @@ export function placeComponents(
coordinate: Coordinate,
layout: Layout,
): void {
- const positionComponents = group(components, (d) => d.position);
+ // Group components by plane & position.
+ const positionComponents = group(
+ components,
+ (d) => `${d.plane || 'xy'}-${d.position}`,
+ );
const {
paddingLeft,
paddingRight,
@@ -332,8 +380,135 @@ export function placeComponents(
insetTop,
height,
width,
+ depth,
} = layout;
+ const planes = {
+ xy: createSection({
+ width,
+ height,
+ paddingLeft,
+ paddingRight,
+ paddingTop,
+ paddingBottom,
+ marginLeft,
+ marginTop,
+ marginBottom,
+ marginRight,
+ innerHeight,
+ innerWidth,
+ insetBottom,
+ insetLeft,
+ insetRight,
+ insetTop,
+ }),
+ yz: createSection({
+ width: depth,
+ height: height,
+ paddingLeft: 0,
+ paddingRight: 0,
+ paddingTop: 0,
+ paddingBottom: 0,
+ marginLeft: 0,
+ marginTop: 0,
+ marginBottom: 0,
+ marginRight: 0,
+ innerWidth: depth,
+ innerHeight: height,
+ insetBottom: 0,
+ insetLeft: 0,
+ insetRight: 0,
+ insetTop: 0,
+ }),
+ xz: createSection({
+ width,
+ height: depth,
+ paddingLeft: 0,
+ paddingRight: 0,
+ paddingTop: 0,
+ paddingBottom: 0,
+ marginLeft: 0,
+ marginTop: 0,
+ marginBottom: 0,
+ marginRight: 0,
+ innerWidth: width,
+ innerHeight: depth,
+ insetBottom: 0,
+ insetLeft: 0,
+ insetRight: 0,
+ insetTop: 0,
+ }),
+ };
+
+ for (const [key, components] of positionComponents.entries()) {
+ const [plane, position] = key.split('-') as [GuideComponentPlane, GCP];
+ const area = planes[plane][position];
+
+ /**
+ * @description non-entity components: axis in the center, inner, outer, component in the center
+ * @description entity components: other components
+ * @description no volume components take up no extra space
+ */
+
+ const [nonEntityComponents, entityComponents] = divide(
+ components,
+ (component) => {
+ if (typeof component.type !== 'string') return false;
+ if (position === 'center') return true;
+ if (
+ component.type.startsWith('axis') &&
+ ['inner', 'outer'].includes(position)
+ ) {
+ return true;
+ }
+ return false;
+ },
+ );
+
+ if (nonEntityComponents.length) {
+ placeNonEntityComponents(nonEntityComponents, coordinate, area, position);
+ }
+ if (entityComponents.length) {
+ placePaddingArea(components, coordinate, area);
+ }
+ }
+}
+
+function createSection({
+ width,
+ height,
+ paddingLeft,
+ paddingRight,
+ paddingTop,
+ paddingBottom,
+ marginLeft,
+ marginTop,
+ marginBottom,
+ marginRight,
+ innerHeight,
+ innerWidth,
+ insetBottom,
+ insetLeft,
+ insetRight,
+ insetTop,
+}: {
+ width: number;
+ height: number;
+ paddingLeft: number;
+ paddingRight: number;
+ paddingTop: number;
+ paddingBottom: number;
+ marginLeft: number;
+ marginTop: number;
+ marginBottom: number;
+ marginRight: number;
+ innerHeight: number;
+ innerWidth: number;
+ insetBottom: number;
+ insetLeft: number;
+ insetRight: number;
+ insetTop: number;
+}): Section {
const pl = paddingLeft + marginLeft;
const pt = paddingTop + marginTop;
const pr = paddingRight + marginRight;
@@ -349,7 +524,7 @@ export function placeComponents(
null,
];
- const section: Section = {
+ const xySection: Section = {
top: [pl, 0, innerWidth, pt, 'vertical', true, ascending],
right: [width - pr, pt, pr, innerHeight, 'horizontal', false, ascending],
bottom: [pl, height - pb, innerWidth, pb, 'vertical', false, ascending],
@@ -379,37 +554,7 @@ export function placeComponents(
outer: centerSection,
};
- for (const [position, components] of positionComponents.entries()) {
- const area = section[position];
-
- /**
- * @description non-entity components: axis in the center, inner, outer, component in the center
- * @description entity components: other components
- * @description no volume components take up no extra space
- */
-
- const [nonEntityComponents, entityComponents] = divide(
- components,
- (component) => {
- if (typeof component.type !== 'string') return false;
- if (position === 'center') return true;
- if (
- component.type.startsWith('axis') &&
- ['inner', 'outer'].includes(position)
- ) {
- return true;
- }
- return false;
- },
- );
-
- if (nonEntityComponents.length) {
- placeNonEntityComponents(nonEntityComponents, coordinate, area, position);
- }
- if (entityComponents.length) {
- placePaddingArea(components, coordinate, area);
- }
- }
+ return xySection;
}
function placeNonEntityComponents(
diff --git a/src/runtime/plot.ts b/src/runtime/plot.ts
index 9e405b19cc..23d5452922 100644
--- a/src/runtime/plot.ts
+++ b/src/runtime/plot.ts
@@ -33,7 +33,12 @@ import {
VIEW_CLASS_NAME,
} from './constant';
import { coordinate2Transform, createCoordinate } from './coordinate';
-import { computeLayout, computeRoughPlotSize, placeComponents } from './layout';
+import {
+ computeLayout,
+ computeRoughPlotSize,
+ placeComponents,
+ processAxisZ,
+} from './layout';
import { documentOf, useLibrary } from './library';
import { initializeMark } from './mark';
import {
@@ -626,6 +631,9 @@ function initializeState(
// Place components and mutate their bbox.
placeComponents(groupComponents(components), coordinate, layout);
+ // AxisZ need a copy of axisX and axisY to show grids in X-Z & Y-Z planes.
+ processAxisZ(components);
+
// Scale from marks and components.
const scaleInstance = {};
diff --git a/src/runtime/render.ts b/src/runtime/render.ts
index 8f03b53c05..a55e1a2df3 100644
--- a/src/runtime/render.ts
+++ b/src/runtime/render.ts
@@ -70,7 +70,7 @@ export function render(
},
): HTMLElement {
// Initialize the context if it is not provided.
- const { width = 640, height = 480, theme } = options;
+ const { width = 640, height = 480, depth = 0, theme } = options;
if (!theme) {
error(
'ChartOptions.theme is required, such as `const chart = new Chart({ theme: "classic"})`.',
@@ -93,9 +93,14 @@ export function render(
const selection = select(canvas.document.documentElement);
canvas.ready
.then(() =>
- plot({ ...keyed, width, height }, selection, library, context),
+ plot({ ...keyed, width, height, depth }, selection, library, context),
)
.then(() => {
+ // Place the center of whole scene at z axis' origin.
+ if (depth) {
+ canvas!.document.documentElement.translate(0, 0, -depth / 2);
+ }
+
// Wait for the next tick.
canvas.requestAnimationFrame(() => {
emitter.emit(ChartEvent.AFTER_RENDER);
diff --git a/src/runtime/types/common.ts b/src/runtime/types/common.ts
index e78097f2be..b8db939326 100644
--- a/src/runtime/types/common.ts
+++ b/src/runtime/types/common.ts
@@ -126,6 +126,7 @@ export type Channel = {
};
export type Vector2 = [number, number];
+export type Vector3 = [number, number, number];
export type BBox = {
x?: number;
@@ -152,6 +153,7 @@ export type GuideComponentPosition =
| GuideCompositePosition;
export type GuideComponentOrientation = 'horizontal' | 'vertical' | number;
+export type GuideComponentPlane = 'xy' | 'xz' | 'yz';
export type Layout = {
paddingLeft?: number;
@@ -173,6 +175,8 @@ export type Layout = {
marginRight?: number;
x?: number;
y?: number;
+ z?: number;
+ depth?: number;
};
export type Direction = 'horizontal' | 'vertical' | 'center';
diff --git a/src/runtime/types/component.ts b/src/runtime/types/component.ts
index 6e7a7b7393..f4bbf52909 100644
--- a/src/runtime/types/component.ts
+++ b/src/runtime/types/component.ts
@@ -1,4 +1,4 @@
-import { Coordinate, Transformation } from '@antv/coord';
+import { Coordinate, Transformation, Transformation3D } from '@antv/coord';
import EventEmitter from '@antv/event-emitter';
import { DisplayObject, IAnimation as GAnimation, IDocument } from '@antv/g';
import {
@@ -9,6 +9,7 @@ import {
IndexedValue,
Vector2,
G2MarkState,
+ GuideComponentPlane,
} from './common';
import { DataComponent } from './data';
import { Encode, EncodeComponent } from './encode';
@@ -115,6 +116,13 @@ export type CoordinateComponent> = G2BaseComponent<
CoordinateProps
>;
+export type Coordinate3DTransform = Transformation3D[];
+export type Coordinate3DProps = {
+ transform?: boolean;
+};
+export type Coordinate3DComponent> =
+ G2BaseComponent;
+
export type Palette = string[];
export type PaletteComponent> = G2BaseComponent<
Palette,
@@ -169,6 +177,7 @@ export type GuideComponent = (context: GuideComponentContext) => DisplayObject;
export type GuideComponentProps = {
defaultPosition?: GuideComponentPosition;
+ defaultPlane?: GuideComponentPlane;
defaultOrientation?: GuideComponentOrientation;
defaultSize?: number;
defaultOrder?: number;
diff --git a/src/runtime/types/options.ts b/src/runtime/types/options.ts
index 617a655e22..c932b30cd1 100644
--- a/src/runtime/types/options.ts
+++ b/src/runtime/types/options.ts
@@ -4,6 +4,7 @@ import { Canvas, IAnimation as GAnimation } from '@antv/g';
import {
G2Title,
G2ViewDescriptor,
+ GuideComponentPlane,
GuideComponentPosition,
Layout,
Primitive,
@@ -32,6 +33,7 @@ import { TransformComponent } from './transform';
export type G2ViewTree = {
width?: number;
height?: number;
+ depth?: number;
} & Node;
export type Node = {
@@ -68,8 +70,10 @@ export type G2View = {
key?: string;
x?: number;
y?: number;
+ z?: number;
width?: number;
height?: number;
+ depth?: number;
padding?: number;
paddingLeft?: number;
paddingRight?: number;
@@ -186,6 +190,7 @@ export type G2GuideComponentOptions = G2BaseComponentOptions<
{
scale?: G2ScaleOptions;
position?: GuideComponentPosition;
+ plane?: GuideComponentPlane;
size?: number;
order?: number;
zIndex?: number;
diff --git a/src/shape/index.ts b/src/shape/index.ts
index 6a66814ccf..986e365d7f 100644
--- a/src/shape/index.ts
+++ b/src/shape/index.ts
@@ -29,6 +29,8 @@ export { Square as PointSquare } from './point/square';
export { Tick as PointTick } from './point/tick';
export { Triangle as PointTriangle } from './point/triangle';
export { TriangleDown as PointTriangleDown } from './point/triangleDown';
+export { Sphere } from './point3D/sphere';
+export { Cube } from './point3D/cube';
export { Vector as VectorShape } from './vector/vector';
export { Text as TextShape } from './text/text';
export { Badge as TextBadge } from './text/badge';
diff --git a/src/shape/point3D/cube.ts b/src/shape/point3D/cube.ts
new file mode 100644
index 0000000000..ebd4ee2fb6
--- /dev/null
+++ b/src/shape/point3D/cube.ts
@@ -0,0 +1,68 @@
+import { MeshPhongMaterial, CubeGeometry, Mesh } from '@antv/g-plugin-3d';
+import { applyStyle, getOrigin, toOpacityKey } from '../utils';
+import { ShapeComponent as SC } from '../../runtime';
+import { select } from '../../utils/selection';
+
+const GEOMETRY_SIZE = 5;
+
+export type CubeOptions = Record;
+
+/**
+ * @see https://g.antv.antgroup.com/api/3d/geometry#cubegeometry
+ */
+export const Cube: SC = (options, context) => {
+ // Render border only when colorAttribute is stroke.
+ const { ...style } = options;
+
+ // @ts-ignore
+ if (!context.cubeGeometry) {
+ const renderer = context.canvas.getConfig().renderer;
+ const plugin = renderer.getPlugin('device-renderer');
+ const device = plugin.getDevice();
+ // create a sphere geometry
+ // @ts-ignore
+ context.cubeGeometry = new CubeGeometry(device, {
+ width: GEOMETRY_SIZE * 2,
+ height: GEOMETRY_SIZE * 2,
+ depth: GEOMETRY_SIZE * 2,
+ });
+ // create a material with Phong lighting model
+ // @ts-ignore
+ context.cubeMaterial = new MeshPhongMaterial(device);
+ }
+
+ return (points, value, defaults) => {
+ const { color: defaultColor } = defaults;
+ const { color = defaultColor, transform, opacity } = value;
+ const [cx, cy, cz] = getOrigin(points);
+ const r = value.size;
+ const finalRadius = r || style.r || defaults.r;
+
+ const cube = new Mesh({
+ style: {
+ x: cx,
+ y: cy,
+ z: cz,
+ // @ts-ignore
+ geometry: context.cubeGeometry,
+ // @ts-ignore
+ material: context.cubeMaterial,
+ },
+ });
+ cube.setOrigin(0, 0, 0);
+ const scaling = finalRadius / GEOMETRY_SIZE;
+ cube.scale(scaling);
+
+ return select(cube)
+ .call(applyStyle, defaults)
+ .style('fill', color)
+ .style('transform', transform)
+ .style(toOpacityKey(options), opacity)
+ .call(applyStyle, style)
+ .node();
+ };
+};
+
+Cube.props = {
+ defaultMarker: 'cube',
+};
diff --git a/src/shape/point3D/sphere.ts b/src/shape/point3D/sphere.ts
new file mode 100644
index 0000000000..07a061b31c
--- /dev/null
+++ b/src/shape/point3D/sphere.ts
@@ -0,0 +1,68 @@
+import { MeshPhongMaterial, SphereGeometry, Mesh } from '@antv/g-plugin-3d';
+import { applyStyle, getOrigin, toOpacityKey } from '../utils';
+import { ShapeComponent as SC } from '../../runtime';
+import { select } from '../../utils/selection';
+
+export type SphereOptions = Record;
+
+const GEOMETRY_SIZE = 5;
+
+/**
+ * @see https://g.antv.antgroup.com/api/3d/geometry#spheregeometry
+ */
+export const Sphere: SC = (options, context) => {
+ // Render border only when colorAttribute is stroke.
+ const { ...style } = options;
+
+ // @ts-ignore
+ if (!context.sphereGeometry) {
+ const renderer = context.canvas.getConfig().renderer;
+ const plugin = renderer.getPlugin('device-renderer');
+ const device = plugin.getDevice();
+ // create a sphere geometry
+ // @ts-ignore
+ context.sphereGeometry = new SphereGeometry(device, {
+ radius: GEOMETRY_SIZE,
+ latitudeBands: 32,
+ longitudeBands: 32,
+ });
+ // create a material with Phong lighting model
+ // @ts-ignore
+ context.sphereMaterial = new MeshPhongMaterial(device);
+ }
+
+ return (points, value, defaults) => {
+ const { color: defaultColor } = defaults;
+ const { color = defaultColor, transform, opacity } = value;
+ const [cx, cy, cz] = getOrigin(points);
+ const r = value.size;
+ const finalRadius = r || style.r || defaults.r;
+
+ const sphere = new Mesh({
+ style: {
+ x: cx,
+ y: cy,
+ z: cz,
+ // @ts-ignore
+ geometry: context.sphereGeometry,
+ // @ts-ignore
+ material: context.sphereMaterial,
+ },
+ });
+ sphere.setOrigin(0, 0, 0);
+ const scaling = finalRadius / GEOMETRY_SIZE;
+ sphere.scale(scaling);
+
+ return select(sphere)
+ .call(applyStyle, defaults)
+ .style('fill', color)
+ .style('transform', transform)
+ .style(toOpacityKey(options), opacity)
+ .call(applyStyle, style)
+ .node();
+ };
+};
+
+Sphere.props = {
+ defaultMarker: 'sphere',
+};
diff --git a/src/shape/utils.ts b/src/shape/utils.ts
index 797dcf9cf1..799daaa9e6 100644
--- a/src/shape/utils.ts
+++ b/src/shape/utils.ts
@@ -3,7 +3,7 @@ import { Linear } from '@antv/scale';
import { lowerFirst } from '@antv/util';
import { extent } from 'd3-array';
import { Path as D3Path } from 'd3-path';
-import { G2Theme, Primitive, Vector2 } from '../runtime';
+import { G2Theme, Primitive, Vector2, Vector3 } from '../runtime';
import { indexOf } from '../utils/array';
import { isPolar, isTranspose } from '../utils/coordinate';
import { Selection } from '../utils/selection';
@@ -212,8 +212,8 @@ export function getTransform(coordinate, value) {
return `translate(${center[0]}, ${center[1]}) ${suffix || ''}`;
}
-export function getOrigin(points: Vector2[]) {
+export function getOrigin(points: (Vector2 | Vector3)[]) {
if (points.length === 1) return points[0];
- const [[x0, y0], [x2, y2]] = points;
- return [(x0 + x2) / 2, (y0 + y2) / 2];
+ const [[x0, y0, z0 = 0], [x2, y2, z2 = 0]] = points;
+ return [(x0 + x2) / 2, (y0 + y2) / 2, (z0 + z2) / 2];
}
diff --git a/src/spec/component.ts b/src/spec/component.ts
index 24ccf2b065..a885ac7b79 100644
--- a/src/spec/component.ts
+++ b/src/spec/component.ts
@@ -33,7 +33,7 @@ export type TitleComponent = {
} & UsePrefix<'title' | 'subtitle', Record>;
export type AxisComponent = {
- type?: 'axisX' | 'axisY';
+ type?: 'axisX' | 'axisY' | 'axisZ';
tickCount?: number;
labelFormatter?: any;
tickFilter?: any;
diff --git a/src/spec/composition.ts b/src/spec/composition.ts
index 65ec111a93..5b90a792bd 100644
--- a/src/spec/composition.ts
+++ b/src/spec/composition.ts
@@ -42,8 +42,10 @@ export type ViewComposition = {
type?: 'view';
x?: number;
y?: number;
+ z?: number;
width?: number;
height?: number;
+ depth?: number;
data?: Data;
key?: string;
class?: string;
diff --git a/src/spec/coordinate.ts b/src/spec/coordinate.ts
index af96beded2..3928df5f94 100644
--- a/src/spec/coordinate.ts
+++ b/src/spec/coordinate.ts
@@ -7,6 +7,7 @@ export type Coordinate =
| ThetaCoordinate
| CustomCoordinate
| CartesianCoordinate
+ | Cartesian3DCoordinate
| ParallelCoordinate
| RadialCoordinate
| RadarCoordinate
@@ -18,6 +19,7 @@ export type CoordinateTypes =
| 'transpose'
| 'theta'
| 'cartesian'
+ | 'cartesian3D'
| 'parallel'
| 'fisheye'
| 'radial'
@@ -70,6 +72,10 @@ export type CartesianCoordinate = BaseCoordinate<{
type?: 'cartesian';
}>;
+export type Cartesian3DCoordinate = BaseCoordinate<{
+ type?: 'cartesian3D';
+}>;
+
export type ParallelCoordinate = BaseCoordinate<{
type?: 'parallel';
}>;
diff --git a/src/spec/index.ts b/src/spec/index.ts
index 4c2a32da86..1e7895d164 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;
+ depth?: number;
autoFit?: boolean;
};
diff --git a/src/spec/mark.ts b/src/spec/mark.ts
index 3679f297a7..170cb41bb6 100644
--- a/src/spec/mark.ts
+++ b/src/spec/mark.ts
@@ -89,6 +89,7 @@ export type MarkTypes =
export type ChannelTypes =
| 'x'
| 'y'
+ | 'z'
| 'x1'
| 'y1'
| 'series'
@@ -114,7 +115,12 @@ export type ChannelTypes =
| 'exitDelay'
| `position${number}`;
-export type PositionChannelTypes = 'x' | 'y' | 'position' | `position${number}`;
+export type PositionChannelTypes =
+ | 'x'
+ | 'y'
+ | 'z'
+ | 'position'
+ | `position${number}`;
export type AtheisticChanelTypes = 'size' | 'color' | 'shape' | 'opacity';
diff --git a/src/transform/index.ts b/src/transform/index.ts
index bb88001baf..f98359bd90 100644
--- a/src/transform/index.ts
+++ b/src/transform/index.ts
@@ -3,6 +3,7 @@ export { MaybeStackY } from './maybeStackY';
export { MaybeTitle } from './maybeTitle';
export { MaybeZeroX } from './maybeZeroX';
export { MaybeZeroY } from './maybeZeroY';
+export { MaybeZeroZ } from './maybeZeroZ';
export { MaybeSize } from './maybeSize';
export { MaybeKey } from './maybeKey';
export { MaybeSeries } from './maybeSeries';
diff --git a/src/transform/maybeZeroZ.ts b/src/transform/maybeZeroZ.ts
new file mode 100644
index 0000000000..aeeae03c48
--- /dev/null
+++ b/src/transform/maybeZeroZ.ts
@@ -0,0 +1,25 @@
+import { deepMix } from '@antv/util';
+import { TransformComponent as TC } from '../runtime';
+import { inferredColumn, constant } from './utils/helper';
+
+export type MaybeZeroZOptions = Record;
+
+/**
+ * Add zero constant encode for z channel.
+ */
+export const MaybeZeroZ: TC = () => {
+ return (I, mark) => {
+ const { encode } = mark;
+ const { z } = encode;
+ if (z !== undefined) return [I, mark];
+ return [
+ I,
+ deepMix({}, mark, {
+ encode: { z: inferredColumn(constant(I, 0)) },
+ scale: { z: { guide: null } },
+ }),
+ ];
+ };
+};
+
+MaybeZeroZ.props = {};
diff --git a/src/utils/size.ts b/src/utils/size.ts
index db4fbb92e5..10592ea8ba 100644
--- a/src/utils/size.ts
+++ b/src/utils/size.ts
@@ -3,6 +3,7 @@ import { G2View } from '../runtime';
type Size = {
width: number;
height: number;
+ depth?: number;
};
const parseInt10 = (d: string) => (d ? parseInt(d) : 0);
@@ -28,6 +29,7 @@ export function getContainerSize(container: HTMLElement): Size {
return {
width: wrapperWidth - widthPadding,
height: wrapperHeight - heightPadding,
+ depth: wrapperWidth,
};
}