Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

使用threejs加d3js从零开始构建3d图表 #25

Open
zp1112 opened this issue Dec 14, 2018 · 0 comments
Open

使用threejs加d3js从零开始构建3d图表 #25

zp1112 opened this issue Dec 14, 2018 · 0 comments
Labels

Comments

@zp1112
Copy link
Owner

zp1112 commented Dec 14, 2018

实现一个带地图和三维坐标的柱状图表

实现效果如下
image
点击update按钮切换
image

开始

用到的插件

<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.20/topojson.min.js"></script>
<script src="https://threejs.org/build/three.js"></script>  
<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/TweenMax.min.js"></script>
<script src="./SVGLoader.js"></script>
<script src="//d3js.org/queue.v1.min.js"></script>
  • D3.js是一个用于根据数据操作文档的JavaScript库。 D3可帮助您使用HTML,SVG和CSS将数据变为现实。
  • TopoJSON是对拓扑进行编码的GeoJSON的扩展。
  • Three.js是一个跨浏览器的脚本,使用JavaScript函数库或API来在网页浏览器中创建和展示动画的三维计算机图形
  • TweenMax动画库
  • SVGLoader是threejs提供的加载svg的插件
  • queue使用指定的并发创建队列对象。添加到队列的任务是并行处理的(最高可达并发限制)

第一步,创建场景,添加相机

const width = window.innerWidth;  
const height = window.innerHeight;
const scene = new THREE.Scene();      //创建场景  
 scene.background = new THREE.Color( 0x000000 );

// 添加平视相机,比较美观 
function orthCamera(){
            camera = new THREE.OrthographicCamera(window.innerWidth/-1.5,window.innerWidth/1.5,
            window.innerHeight/1.5,window.innerHeight/-1.5,100,10000);
            camera.position.set(500,500, 800);//设置相机坐标
            camera.lookAt({x: 0, y: 0, z: 0});//让相机指向场景中心
}
orthCamera();
 // 渲染
const renderer = new THREE.WebGLRenderer({antialias : true});     //创建渲染器(并设置抗锯齿属性)  
renderer.setSize(width, height);    //设置渲染器的大小
 document.body.appendChild(renderer.domElement);     //添加渲染器的DOM元素到body中  

// 加入控制器
const controls = new THREE.OrbitControls( camera, renderer.domElement );
controls.screenSpacePanning = true;

function animate(time) {
            requestAnimationFrame( animate );
            renderer.render( scene, camera );
 }
animate();

至此已经创建好了场景空间,接下来要做的就是添加物体。

添加坐标轴

坐标轴分为三个面,六个坐标刻度,每个面两个刻度,相对的刻度相同

const geometryLeft = new THREE.Geometry();    //创建geometry  
geometryLeft.vertices.push(new THREE.Vector3(0, 0 ,0));  //添加顶点  
 geometryLeft.vertices.push(new THREE.Vector3(19*50, 0, 0));  

const geometryLeft1 = new THREE.Geometry();    //创建geometry  
geometryLeft1.vertices.push(new THREE.Vector3(0, 0 ,0));  //添加顶点  
geometryLeft1.vertices.push(new THREE.Vector3(19*30, 0, 0)); 
          
for(let i=0;i<10;i++){  
            //var mesh = new THREE.Mesh(geometry, material);  
            const line1 = new THREE.Line(geometryLeft, new THREE.LineBasicMaterial({color:0xffffff}));     //利用geometry和material创建line  
            line1.position.z = i*60;   //设置line的位置  
            scene.add(line1);    //将line添加到场景中  
              
            const line11 = new THREE.Line(geometryLeft1, new THREE.LineBasicMaterial({color:0xffffff}));  
            line11.position.x = i*100;  
            line11.rotation.y = -Math.PI/2;    //绕y轴旋转90度  
            scene.add(line11);  
 }

const geometryBack = new THREE.Geometry();    //创建geometry  
geometryBack.vertices.push(new THREE.Vector3(0, 0 ,0));  //添加顶点  
geometryBack.vertices.push(new THREE.Vector3(0, 0, 19*30));  
const geometryBack1 = new THREE.Geometry();    //创建geometry  
geometryBack1.vertices.push(new THREE.Vector3(0, 0 ,0));  //添加顶点  
geometryBack1.vertices.push(new THREE.Vector3(0, 0, 19*20)); 
for(let i=0;i<10;i++){  
            //var mesh = new THREE.Mesh(geometry, material);  
            const line2 = new THREE.Line(geometryBack, new THREE.LineBasicMaterial({color:0xffffff}));     //利用geometry和material创建line  
            line2.position.y = i*40;   //设置line的位置  
            scene.add(line2);    //将line添加到场景中  
              
            const line22 = new THREE.Line(geometryBack1, new THREE.LineBasicMaterial({color:0xffffff}));  
            line22.position.z = i*60;  
            line22.rotation.x = -Math.PI/2;    //绕y轴旋转90度  
            scene.add(line22);  
 }

const geometryBottom = new THREE.Geometry();    //创建geometry  
geometryBottom.vertices.push(new THREE.Vector3(0, 0 ,0));  //添加顶点  
geometryBottom.vertices.push(new THREE.Vector3(19*50, 0, 0)); 
const geometryBottom1 = new THREE.Geometry();    //创建geometry  
geometryBottom1.vertices.push(new THREE.Vector3(0, 0 ,0));  //添加顶点  
geometryBottom1.vertices.push(new THREE.Vector3(19*20, 0, 0)); 
for(let i=0;i<10;i++){  
            //var mesh = new THREE.Mesh(geometry, material);  
            const line3 = new THREE.Line(geometryBottom, new THREE.LineBasicMaterial({color:0xffffff}));     //利用geometry和material创建line  
            line3.position.y = i*40;   //设置line的位置  
            scene.add(line3);    //将line添加到场景中  
              
            const line33 = new THREE.Line(geometryBottom1, new THREE.LineBasicMaterial({color:0xffffff}));  
            line33.position.x = i*100;  
            line33.rotation.z = Math.PI/2;    //绕y轴旋转90度  
            scene.add(line33);  
 }
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(100, 100, 50);
scene.add(dirLight);

得到效果
image

添加xz平面上的地图平面

由于坐标轴刻度的y轴将用于柱形图的高度,因此需要根据实际数据推算domain,所以需要放在数据加载后在执行,因此先加载地图数据.
d3提供了geoPath方法可以渲染世界地图,threejs提供了svgLoader加载svg,将svg数据转换成threejs的shape平面
这个地图svg目前是通过在另一个文件使用d3画的,然后在浏览器中将生成的svgcopy下来保存成文件待使用。
image

const svgloader = new THREE.SVGLoader();
svgloader.load( './usasvg.svg', function ( paths ) {
            const group = new THREE.Group();
            const len = paths.length;
            for ( let i = 0; i < len; i ++ ) {
                const path = paths[ i ];
                const material = new THREE.MeshBasicMaterial( {
                    color: path.color,
                    side: THREE.DoubleSide,
                    depthWrite: false
                } );
                const shapes = path.toShapes( true );
                for ( let j = 0; j < shapes.length; j ++ ) {
                    const shape = shapes[ j ];
                    const geometry = new THREE.ShapeBufferGeometry( shape );
                    const mesh = new THREE.Mesh( geometry, material );
                    group.add( mesh );
                }
            }
            group.rotation.x = Math.PI / 2; // 将xy平面上的地图翻转到xz平面
            scene.add( group );
} );

得到效果:
image

添加柱状图

定义几个变量,用于存储object

const scaleHeight = d3.scaleLinear(); // y轴方向的比例尺
const bars = []; // 柱状体
const spheres = []; // 散点

开始加载地图数据,这里的地图和数据都要和之前使用d3生成svg并渲染成shape的那个数据一样.
在body里面添加一个按钮update,用于动态变换柱形图和散点图形态。

<div id="update">update</div>
const projection = d3.geoAlbersUsa().scale(1000);
const path = d3.geoPath().projection(projection);

queue().defer(d3.json, "./us.json").defer(d3.json, "./us-centroid.json").await(ready);
        
function ready(error, data, centroids) {
            const features = topojson.feature(data, data.objects.states).features;
            const centroidsFeatures = centroids.features;
            const len = centroidsFeatures.length;
// 在此处决定比例尺的domain
            scaleHeight.domain([0, Math.max(...centroidsFeatures.map(row => row.properties.population)) / 10000]).range([0, 360])
            renderBars();
            renderSpheres();
            
            const update = document.getElementById('update');
            update.addEventListener('click', handleUpdateClick, false);
            var curType = 'sphere';

            function renderBars() {
                for (var i = 0; i < len; i ++) {
                    var centroid = path.centroid(centroidsFeatures[i]),
                            x = centroid[0],
                            y = centroid[1];
                    var barGeometry = new THREE.BoxGeometry(20, 2, 20);
// 此处的boxGeometry生成的box的y轴会随着高度正负伸长,所以需要矩阵转换来矫正一下,使得y是其一半的数值
                    barGeometry .applyMatrix(new THREE.Matrix4().makeTranslation(0,1, y));

                    var barMaterial = new THREE.MeshPhongMaterial({
                        color: 0x00ff00
                    });
// 数值比较大,我们除以10000
                    var barHeight = Math.floor(centroidsFeatures[i].properties.population / 10000 );
                    bar = new THREE.Mesh(barGeometry , barMaterial);

                    bar.position.x = x;
                    bar.barHeight= barHeight;
                    bar.customType = 'bar';
                    bar.customValue = Math.floor(centroidsFeatures[i].properties.population);

                    scene.add(bar);
                    bars.push(bar);
                }
// 使用TweenMax动画库实现动画
                for (var i = 0; i < bars.length; i++) {
                    var tween = new TweenMax.to(bars[i].scale, 1, {
                        ease: Elastic.easeOut.config(1, 1),
                        y: scaleHeight(bars[i].cubeHeight / 2)
                    });
                }
            }
            function renderSpheres() {
                for (var i = 0; i < len; i ++) {
                    var centroid = path.centroid(centroidsFeatures[i]),
                            x = centroid[0],
                            y = centroid[1];
                    var sphereGeometry = new THREE.SphereGeometry(5);
                    sphereGeometry.applyMatrix(new THREE.Matrix4().makeTranslation(0,1, y));

                    var sphereMaterial = new THREE.MeshBasicMaterial({
                        color: 0x00ff00
                    });

                    var sphereHeight = Math.floor(centroidsFeatures[i].properties.population / 10000 );
                    sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);

                    sphere.position.x = x;
                    sphere.sphereHeight= sphereHeight;
                    sphere.customType = 'sphere';
                    sphere.customValue = Math.floor(centroidsFeatures[i].properties.population);
                    sphere.visible = false;

                    scene.add(sphere);
                    spheres.push(sphere);
                }
                for (var i = 0; i < spheres.length; i++) {
                    new TweenMax.to(spheres[i].position, 1, {
                        ease: Elastic.easeOut.config(1, 1),
                        y: scaleHeight(spheres[i].sphereHeight)
                    });
                }
            }
            function showSpheres(i) {
                curType = 'sphere';
                new TweenMax.to(bars[i].scale, 1, {
                    ease: Elastic.easeOut.config(1, 1),
                    y: 1
                });
                cubes[i].visible = false;
                spheres[i].visible = true;
                new TweenMax.to(spheres[i].position, 1, {
                    ease: Elastic.easeOut.config(1, 1),
                    y: scaleHeight(spheres[i].barHeight)
                });
            }
            function showBars(i) {
                curType = 'bar';
                new TweenMax.to(spheres[i].position, 1, {
                    ease: Elastic.easeOut.config(1, 1),
                    y: 1
                });
                spheres[i].visible = false;
                cubes[i].visible = true;
                new TweenMax.to(bars[i].scale, 1, {
                    ease: Elastic.easeOut.config(1, 1),
                    y: scaleHeight(bars[i].barHeight/ 2)
                });
            }
            handleUpdateClick();
            function handleUpdateClick() {
                if (curType === 'bar') {
                    for (var i = 0; i < cubes.length; i++) {
                        showSpheres(i);
                        // var tween = new TweenMax.to(bars[i].scale, 1, {
                        //     // ease: Elastic.easeOut.config(1, 1),
                        //     y: 1,
                        //     onComplete: showSpheres,
                        //     onCompleteParams:[i]
                        // });
                    }
                } else {
                    for (var i = 0; i < spheres.length; i++) {
                        showBars(i)
                        // var tween1 = new TweenMax.to(spheres[i].position, 1, {
                        //     // ease: Elastic.easeOut.config(1, 1),
                        //     y: 0,
                        //     onComplete: showBars,
                        //     onCompleteParams:[i]
                        // });
                    }
                }
            }
        }

得到效果:
image

添加刻度文字

threejs里面使用THREE.FontLoader加载threejs格式的字体,在官网可以下载。

function createText() {
            const textLoader = new THREE.FontLoader();
            textLoader.load(
                'https://threejs.org/examples/fonts/helvetiker_regular.typeface.json',
                function (font) {
                    // left top text
                    const options = {
                        size: 18,
                        height: 0,
                        font, // “引用js字体必须换成英文”
                        bevelThickness: 1,
                        bevelSize: 1,
                        bevelSegments: 1,
                        curveSegments: 50,
                        steps: 1
                    }
                    function createText(positions, n = 10) {
                        for(let i=0;i<n;i++){
                            // 使用TextBufferGeometry比TextGeometry快
                            const textLeftTop = new THREE.TextBufferGeometry(positions.text ? positions.text(i) : JSON.stringify(i * positions.n), options);
                            const textMeshLeftTop = new THREE.Mesh(textLeftTop, new THREE.MeshBasicMaterial());
                            textMeshLeftTop.position.x = typeof positions.x === 'function' ? positions.x(i) : positions.x;
                            textMeshLeftTop.position.y = typeof positions.y === 'function' ? positions.y(i) : positions.y;
                            textMeshLeftTop.position.z = typeof positions.z === 'function' ? positions.z(i) : positions.z;
                            textGroup.push(textMeshLeftTop);
                            scene.add(textMeshLeftTop);
                        }
                    }
                    createText({
                        n: 60,
                        x: 0,
                        y: 400,
                        z: function(i) {
                            return i * 60
                        }
                    });
                    createText({
                        text: function(i) {
                            return JSON.stringify(Math.floor(scaleHeight.invert(i * 40) * 1000));
                        },
                        n: 40,
                        x: 0,
                        y: function(i) {
                            return i * 40
                        },
                        z: 600
                    });
                    createText({
                        n: 100,
                        x: function(i) {
                            return i * 100
                        },
                        y: 0,
                        z: 600
                    });
                    createText({
                        n: 100,
                        x: function(i) {
                            return i * 100
                        },
                        y: 400,
                        z: 0
                    });
                    createText({
                        text: function(i) {
                            return JSON.stringify(Math.floor(scaleHeight.invert(i * 40) * 1000));
                        },
                        n: 40,
                        x: 1000,
                        y: function(i) {
                            return i * 40
                        },
                        z: 0
                    });
                    createText({
                        n: 60,
                        x: 1000,
                        y: 0,
                        z: function(i) {
                            return i * 60
                        }
                    });
                }
            );
        }

createText的调用需要放到计算出scaleHeight的domain的时候调用,但是放那里调用会使得图形渲染产生卡顿,暂时还没想到好的办法。

createText();

得到效果:
image

@zp1112 zp1112 added the threejs label Dec 14, 2018
@zp1112 zp1112 changed the title 使用threejs加d3js从零开始构建3d图标 使用threejs加d3js从零开始构建3d图表 Mar 27, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant