From a91214abef813ca0a5c2c354cd37f2a59103feaa Mon Sep 17 00:00:00 2001 From: xiaoiver Date: Fri, 18 Aug 2023 16:55:53 +0800 Subject: [PATCH] fix: skip resizing canvas if width or height remains the same (#5432) --- .../chart-render-3d-scatter-plot-legend.ts | 126 +++++++++++++++ __tests__/plots/api/index.ts | 1 + site/docs/manual/extra-topics/3d-charts.zh.md | 145 ++++++++++++++++-- .../threed/scatter/demo/custom-legend.ts | 120 +++++++++++++++ .../{3d => threed}/scatter/demo/meta.json | 8 + .../scatter/demo/orthographic-projection.ts | 2 +- .../scatter/demo/perspective-projection.ts | 2 +- .../scatter/demo/sphere-shape.ts | 4 +- .../{3d => threed}/scatter/index.en.md | 0 .../{3d => threed}/scatter/index.zh.md | 0 src/mark/point3D.ts | 2 +- src/runtime/render.ts | 10 +- 12 files changed, 404 insertions(+), 16 deletions(-) create mode 100644 __tests__/plots/api/chart-render-3d-scatter-plot-legend.ts create mode 100644 site/examples/threed/scatter/demo/custom-legend.ts rename site/examples/{3d => threed}/scatter/demo/meta.json (77%) rename site/examples/{3d => threed}/scatter/demo/orthographic-projection.ts (98%) rename site/examples/{3d => threed}/scatter/demo/perspective-projection.ts (98%) rename site/examples/{3d => threed}/scatter/demo/sphere-shape.ts (96%) rename site/examples/{3d => threed}/scatter/index.en.md (100%) rename site/examples/{3d => threed}/scatter/index.zh.md (100%) diff --git a/__tests__/plots/api/chart-render-3d-scatter-plot-legend.ts b/__tests__/plots/api/chart-render-3d-scatter-plot-legend.ts new file mode 100644 index 0000000000..5e655f9047 --- /dev/null +++ b/__tests__/plots/api/chart-render-3d-scatter-plot-legend.ts @@ -0,0 +1,126 @@ +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'; + +// 添加图例 +function legendColor(chart) { + // 创建 Legend 并且挂在图例 + const node = chart.getContainer(); + const legend = document.createElement('div'); + legend.style.display = 'flex'; + node.insertBefore(legend, node.childNodes[0]); + + // 创建并挂载 Items + const { color: scale } = chart.getScale(); + const { domain } = scale.getOptions(); + const items = domain.map((value) => { + const item = document.createElement('div'); + const color = scale.map(value); + item.style.marginLeft = '1em'; + item.innerHTML = ` + + ${value} + `; + return item; + }); + items.forEach((d) => legend.append(d)); + + // 监听事件 + const selectedValues = [...domain]; + const options = chart.options(); + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const value = domain[i]; + item.style.cursor = 'pointer'; + item.onclick = () => { + const index = selectedValues.indexOf(value); + if (index !== -1) { + selectedValues.splice(index, 1); + item.style.opacity = 0.5; + } else { + selectedValues.push(value); + item.style.opacity = 1; + } + changeColor(selectedValues); + }; + } + + // 重新渲染视图 + function changeColor(value) { + const { transform = [] } = options; + const newTransform = [{ type: 'filter', color: { value } }, ...transform]; + chart.options({ + ...options, + transform: newTransform, // 指定新的 transform + scale: { color: { domain } }, + }); + chart.render(); // 重新渲染图表 + } +} + +export function chartRender3dScatterPlotLegend(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('color', 'Origin') + .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(() => { + legendColor(chart); + + 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 }; +} + +chartRender3dScatterPlotLegend.skip = true; diff --git a/__tests__/plots/api/index.ts b/__tests__/plots/api/index.ts index 3ff7aad970..e8d7271902 100644 --- a/__tests__/plots/api/index.ts +++ b/__tests__/plots/api/index.ts @@ -43,3 +43,4 @@ 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'; +export { chartRender3dScatterPlotLegend } from './chart-render-3d-scatter-plot-legend'; diff --git a/site/docs/manual/extra-topics/3d-charts.zh.md b/site/docs/manual/extra-topics/3d-charts.zh.md index 92863f0391..a212247ba5 100644 --- a/site/docs/manual/extra-topics/3d-charts.zh.md +++ b/site/docs/manual/extra-topics/3d-charts.zh.md @@ -11,6 +11,7 @@ order: 11 - 在场景中设置相机 - 添加光源 - 使用相机交互 +- 添加图例 我们暂不支持图例。 @@ -74,9 +75,7 @@ chart .encode('x', 'Horsepower') .encode('y', 'Miles_per_Gallon') .encode('z', 'Weight_in_lbs') - .encode('size', 'Origin') - .encode('color', 'Cylinders') - .encode('shape', 'cube') + .encode('color', 'Origin') .coordinate({ type: 'cartesian3D' }) .scale('x', { nice: true }) .scale('y', { nice: true }) @@ -130,8 +129,7 @@ chart.render().then(() => { .encode('x', 'Horsepower') .encode('y', 'Miles_per_Gallon') .encode('z', 'Weight_in_lbs') - .encode('color', 'Cylinders') - .encode('shape', 'cube') + .encode('color', 'Origin') .coordinate({ type: 'cartesian3D' }) .scale('x', { nice: true }) .scale('y', { nice: true }) @@ -195,8 +193,7 @@ camera.rotate(-20, -20, 0); .encode('x', 'Horsepower') .encode('y', 'Miles_per_Gallon') .encode('z', 'Weight_in_lbs') - .encode('color', 'Cylinders') - .encode('shape', 'cube') + .encode('color', 'Origin') .coordinate({ type: 'cartesian3D' }) .scale('x', { nice: true }) .scale('y', { nice: true }) @@ -273,8 +270,7 @@ canvas.appendChild(light); .encode('x', 'Horsepower') .encode('y', 'Miles_per_Gallon') .encode('z', 'Weight_in_lbs') - .encode('color', 'Cylinders') - .encode('shape', 'cube') + .encode('color', 'Origin') .coordinate({ type: 'cartesian3D' }) .scale('x', { nice: true }) .scale('y', { nice: true }) @@ -310,3 +306,134 @@ canvas.appendChild(light); 3D 场景下的交互和 2D 场景有很大的不同,[g-plugin-control](https://g.antv.antgroup.com/plugins/control) 提供了 3D 场景下基于相机的交互。当我们拖拽画布时,会控制相机绕视点进行旋转操作,而鼠标滚轮的缩放会让相机进行 dolly 操作。 需要注意的是缩放操作在正交投影下是没有效果的,但旋转操作依然有效。 + +## 添加自定义图例 + +你可能注意到在上面的例子中我们刻意关闭了图例: + +```ts +chart.legend(false); +``` + +这是由于 3D 场景中的图形都会受到相机影响,但像图例这样的 HUD 组件更适合独立绘制。参考[自定义图例](/manual/extra-topics/customization#自定义图例legend),我们可以使用 HTML 自定义图例: + +```js | ob { pin: false } +(() => { + // 添加图例 + function legendColor(chart) { + // 创建 Legend 并且挂在图例 + const node = chart.getContainer(); + const legend = document.createElement('div'); + legend.style.display = 'flex'; + node.insertBefore(legend, node.childNodes[0]); + + // 创建并挂载 Items + const { color: scale } = chart.getScale(); + const { domain } = scale.getOptions(); + const items = domain.map((value) => { + const item = document.createElement('div'); + const color = scale.map(value); + item.style.marginLeft = '1em'; + item.innerHTML = ` + + ${value} + `; + return item; + }); + items.forEach((d) => legend.append(d)); + + // 监听事件 + const selectedValues = [...domain]; + const options = chart.options(); + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const value = domain[i]; + item.style.cursor = 'pointer'; + item.onclick = () => { + const index = selectedValues.indexOf(value); + if (index !== -1) { + selectedValues.splice(index, 1); + item.style.opacity = 0.5; + } else { + selectedValues.push(value); + item.style.opacity = 1; + } + changeColor(selectedValues); + }; + } + + // 重新渲染视图 + function changeColor(value) { + const { transform = [] } = options; + const newTransform = [{ type: 'filter', color: { value } }, ...transform]; + chart.options({ + ...options, + transform: newTransform, // 指定新的 transform + scale: { color: { domain } }, + }); + chart.render(); // 重新渲染图表 + } + } + + 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', 'Origin') + .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(() => { + legendColor(chart); + + 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(); +})(); +``` diff --git a/site/examples/threed/scatter/demo/custom-legend.ts b/site/examples/threed/scatter/demo/custom-legend.ts new file mode 100644 index 0000000000..494b0ccfec --- /dev/null +++ b/site/examples/threed/scatter/demo/custom-legend.ts @@ -0,0 +1,120 @@ +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'; + +// 添加图例 +function legendColor(chart) { + // 创建 Legend 并且挂在图例 + const node = chart.getContainer(); + const legend = document.createElement('div'); + legend.style.display = 'flex'; + node.insertBefore(legend, node.childNodes[0]); + + // 创建并挂载 Items + const { color: scale } = chart.getScale(); + const { domain } = scale.getOptions(); + const items = domain.map((value) => { + const item = document.createElement('div'); + const color = scale.map(value); + item.style.marginLeft = '1em'; + item.innerHTML = ` + + ${value} + `; + return item; + }); + items.forEach((d) => legend.append(d)); + + // 监听事件 + const selectedValues = [...domain]; + const options = chart.options(); + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const value = domain[i]; + item.style.cursor = 'pointer'; + item.onclick = () => { + const index = selectedValues.indexOf(value); + if (index !== -1) { + selectedValues.splice(index, 1); + item.style.opacity = 0.5; + } else { + selectedValues.push(value); + item.style.opacity = 1; + } + changeColor(selectedValues); + }; + } + + // 重新渲染视图 + function changeColor(value) { + const { transform = [] } = options; + const newTransform = [{ type: 'filter', color: { value } }, ...transform]; + chart.options({ + ...options, + transform: newTransform, // 指定新的 transform + scale: { color: { domain } }, + }); + chart.render(); // 重新渲染图表 + } +} + +// 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', 'Origin') + .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(() => { + legendColor(chart); + + 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/meta.json b/site/examples/threed/scatter/demo/meta.json similarity index 77% rename from site/examples/3d/scatter/demo/meta.json rename to site/examples/threed/scatter/demo/meta.json index 42dd2cc544..ef61f8f1a1 100644 --- a/site/examples/3d/scatter/demo/meta.json +++ b/site/examples/threed/scatter/demo/meta.json @@ -27,6 +27,14 @@ "en": "Sphere" }, "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*KNCUQqzw2JsAAAAAAAAAAAAADmJ7AQ/original" + }, + { + "filename": "custom-legend.ts", + "title": { + "zh": "自定义图例", + "en": "Customize legend" + }, + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*SmF_Ro5UYcwAAAAAAAAAAAAADmJ7AQ/original" } ] } diff --git a/site/examples/3d/scatter/demo/orthographic-projection.ts b/site/examples/threed/scatter/demo/orthographic-projection.ts similarity index 98% rename from site/examples/3d/scatter/demo/orthographic-projection.ts rename to site/examples/threed/scatter/demo/orthographic-projection.ts index 2583d096e4..96831d1be1 100644 --- a/site/examples/3d/scatter/demo/orthographic-projection.ts +++ b/site/examples/threed/scatter/demo/orthographic-projection.ts @@ -28,7 +28,7 @@ chart .encode('x', 'Horsepower') .encode('y', 'Miles_per_Gallon') .encode('z', 'Weight_in_lbs') - .encode('color', 'Cylinders') + .encode('color', 'Origin') .encode('shape', 'cube') .coordinate({ type: 'cartesian3D' }) .scale('x', { nice: true }) diff --git a/site/examples/3d/scatter/demo/perspective-projection.ts b/site/examples/threed/scatter/demo/perspective-projection.ts similarity index 98% rename from site/examples/3d/scatter/demo/perspective-projection.ts rename to site/examples/threed/scatter/demo/perspective-projection.ts index b858addcaa..2027e66c42 100644 --- a/site/examples/3d/scatter/demo/perspective-projection.ts +++ b/site/examples/threed/scatter/demo/perspective-projection.ts @@ -28,7 +28,7 @@ chart .encode('x', 'Horsepower') .encode('y', 'Miles_per_Gallon') .encode('z', 'Weight_in_lbs') - .encode('color', 'Cylinders') + .encode('color', 'Origin') .encode('shape', 'cube') .coordinate({ type: 'cartesian3D' }) .scale('x', { nice: true }) diff --git a/site/examples/3d/scatter/demo/sphere-shape.ts b/site/examples/threed/scatter/demo/sphere-shape.ts similarity index 96% rename from site/examples/3d/scatter/demo/sphere-shape.ts rename to site/examples/threed/scatter/demo/sphere-shape.ts index f7d68853ef..ed0360c95d 100644 --- a/site/examples/3d/scatter/demo/sphere-shape.ts +++ b/site/examples/threed/scatter/demo/sphere-shape.ts @@ -28,8 +28,8 @@ chart .encode('x', 'Horsepower') .encode('y', 'Miles_per_Gallon') .encode('z', 'Weight_in_lbs') - .encode('size', 'Origin') - .encode('color', 'Cylinders') + .encode('color', 'Origin') + .encode('size', 'Cylinders') .encode('shape', 'sphere') .coordinate({ type: 'cartesian3D' }) .scale('x', { nice: true }) diff --git a/site/examples/3d/scatter/index.en.md b/site/examples/threed/scatter/index.en.md similarity index 100% rename from site/examples/3d/scatter/index.en.md rename to site/examples/threed/scatter/index.en.md diff --git a/site/examples/3d/scatter/index.zh.md b/site/examples/threed/scatter/index.zh.md similarity index 100% rename from site/examples/3d/scatter/index.zh.md rename to site/examples/threed/scatter/index.zh.md diff --git a/src/mark/point3D.ts b/src/mark/point3D.ts index 878aaf2ccf..ec4aa78e30 100644 --- a/src/mark/point3D.ts +++ b/src/mark/point3D.ts @@ -65,7 +65,7 @@ const shape = { }; Point3D.props = { - defaultShape: 'sphere', + defaultShape: 'cube', defaultLabelShape: 'label', composite: false, shape, diff --git a/src/runtime/render.ts b/src/runtime/render.ts index a55e1a2df3..48bb9295a3 100644 --- a/src/runtime/render.ts +++ b/src/runtime/render.ts @@ -84,7 +84,11 @@ export function render( } = context; context.canvas = canvas; context.emitter = emitter; - canvas.resize(width, height); + + const { width: prevWidth, height: prevHeight } = canvas.getConfig(); + if (prevWidth !== width || prevHeight !== height) { + canvas.resize(width, height); + } emitter.emit(ChartEvent.BEFORE_RENDER); @@ -98,7 +102,9 @@ export function render( .then(() => { // Place the center of whole scene at z axis' origin. if (depth) { - canvas!.document.documentElement.translate(0, 0, -depth / 2); + const [x, y] = canvas!.document.documentElement.getPosition(); + // Since `render` method can be called for multiple times, use setPosition instead of translate here. + canvas!.document.documentElement.setPosition(x, y, -depth / 2); } // Wait for the next tick.