【视觉高级篇】27 # 如何实现简单的3D可视化图表:GitHub贡献图表的3D可视化?

news2024/12/29 9:06:46

说明

【跟月影学可视化】学习笔记。

第一步:准备要展现的数据

可以使用这个生成数据:https://github.com/sallar/github-contributions-api

在这里插入图片描述
这里直接使用月影大佬的github提交数据的数据即可

在这里插入图片描述

结构大致如下:

在这里插入图片描述

第二步:用 SpriteJS 渲染数据、完成绘图

SpriteJS v3

SpriteJS 是跨平台的高性能图形系统,它能够支持web、node、桌面应用和小程序的图形绘制和实现各种动画效果。

特性:

  • 像操作DOM对象一样操作画布上的图形元素
  • WebGL2渲染
  • 多图层处理图形、文本、图像渲染
  • DOM事件代理、自定义事件派发
  • 使用ES6+语法和面向对象编程
  • OffscreenCanvas和Web Worker多线程渲染
  • 结构化对象树,对d3引擎友好,能够无缝使用
  • 服务端渲染
  • Vue

注意:需要加入 3d 扩展库加载并渲染3D模型。

在这里插入图片描述
SpriteJS 的 3D 部分,它是基于 OGL 库实现的。SpriteJS 在 OGL 的基础上,对几何体元素进行了类似 DOM 元素的封装。这样创建几何体元素就可以像操作 DOM 一样方便,可以直接用 d3 库的 selection 子模块来进行操作。

1. 创建 Scene 对象

const container = document.getElementById('stage');
// 创建 Scene 对象
const scene = new Scene({
  container,
  displayRatio: 2,
});

2. 创建 Layer 对象

在 SpriteJS 中,一个 Layer 对象就对应于一个 Canvas 画布。

// 创建 Layer 对象:创建了一个 3D(WebGL)上下文的 Canvas 画布
const layer = scene.layer3d('fglayer', {
	ambientColor: [0.5, 0.5, 0.5, 1],
	camera: {
	    fov: 35, // 相机的视角设置为 35 度
	},
});
// 相机坐标位置为(6, 6, 6)
layer.camera.attributes.pos = [6, 6, 6];
// 相机朝向坐标原点
layer.camera.lookAt([0, 0, 0]);

3. 将数据转换成柱状元素

这里借助 d3-selection,d3 是一个数据驱动文档的模型,d3-selection 能够通过数据操作文档树,添加元素节点。

https://github.com/d3/d3/blob/main/API.md

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>如何实现简单的3D可视化图表:GitHub 贡献图表的3D可视化</title>
        <style>
            #stage {
                width: 840px;
                height: 640px;
                border: 1px dashed #fa8072;
            }
        </style>
    </head>
    <body>
        <script src="https://d3js.org/d3.v5.js"></script>
        <div id="stage"></div>
        <script type="module">
            import { Scene } from 'https://unpkg.com/spritejs/dist/spritejs.esm.js';
            import { Cube, Light, shaders } from 'https://unpkg.com/sprite-extend-3d/dist/sprite-extend-3d.esm.js';
            // 获取该日期之前大约一年的数据
            let cache = null;
            async function getData(toDate = new Date()) {
                if(!cache) {
                    // 先从 JSON 文件中读取数据并缓存起来
                    const data = await (await fetch('./assets/github_contributions_akira-cn.json')).json();
                    cache = data.contributions.map((o) => {
                        o.date = new Date(o.date.replace(/-/g, '/'));
                        return o;
                    });
                }
                // 要拿到 toData 日期之前大约一年的数据(52周)
                let start = 0,
                    end = cache.length;
                // 用二分法查找
                while(start < end - 1) {
                    const mid = Math.floor(0.5 * (start + end));
                    const {date} = cache[mid];
                    if(date <= toDate) end = mid;
                    else start = mid;
                }
                // 获得对应的一年左右的数据
                let day;
                if(end >= cache.length) {
                    day = toDate.getDay();
                } else {
                    const lastItem = cache[end];
                    day = lastItem.date.getDay();
                }
                // 根据当前星期几,再往前拿52周的数据
                const len = 7 * 52 + day + 1;
                const ret = cache.slice(end, end + len);
                if(ret.length < len) {
                    // 日期超过了数据范围,补齐数据
                    const pad = new Array(len - ret.length).fill({count: 0, color: '#ebedf0'});
                    ret.push(...pad);
                }
                return ret;
            }

            (async function () {
                const container = document.getElementById('stage');
                // 创建 Scene 对象
                const scene = new Scene({
                    container,
                    displayRatio: 2, // 设置显示分辨率
                });
                // 创建 Layer 对象:创建了一个 3D(WebGL)上下文的 Canvas 画布
                const layer = scene.layer3d('fglayer', {
                    ambientColor: [0.5, 0.5, 0.5, 1],
                    camera: {
                        fov: 35, // 相机的视角设置为 35 度
                    },
                });
                // 相机坐标位置为(6, 6, 6)
                layer.camera.attributes.pos = [6, 6, 6];
                // 相机朝向坐标原点
                layer.camera.lookAt([0, 0, 0]);

                // 创建用来 3D 展示的 WebGL 程序:shaders.GEOMETRY 默认支持 phong 反射模型的一组着色器
                const program = layer.createProgram({
                    vertex: shaders.GEOMETRY.vertex,
                    fragment: shaders.GEOMETRY.fragment,
                });

                // 获取数据
                const dataset = await getData(new Date(2019, 12, 31));
                const max = d3.max(dataset, (a) => {
                    return a.count;
                });

                // 用数据来操作文档树
                const selection = d3.select(layer);
                /**
                 * 设置长方体 Cube 的属性
                 *      长 (width)
                 *      宽 (depth)
                 *      高 (height)
                 *      y 轴的缩放 (scaleY):设置为 d.count 与 max 的比值,值在 0~1 之间
                 *      位置 (pos)坐标:根据数据的索引设置 x 和 z 来决定的。
                 *      长方体的颜色 (colors)
                 * */
                const chart = selection.selectAll('cube')
                    .data(dataset)
                    .enter()
                    .append(() => {
                        return new Cube(program);
                    })
                    .attr('width', 0.14)
                    .attr('depth', 0.14)
                    .attr('height', 1)
                    .attr('scaleY', (d) => {
                        // max 是指一年的提交记录中,提交代码最多那天的数值。
                        return d.count / max;
                    })
                    .attr('pos', (d, i) => {
                        const x0 = -3.8 + 0.0717 + 0.0015;
                        const z0 = -0.5 + 0.05 + 0.0015;
                        const x = x0 + 0.143 * Math.floor(i / 7);
                        const z = z0 + 0.143 * (i % 7);
                        return [x, 0.5 * d.count /max, z];
                    })
                    .attr('colors', (d, i) => {
                        return d.color;
                    });
                
                layer.setOrbit();
            }());
        </script>
    </body>
</html>

效果如下:

在这里插入图片描述

第三步:补充细节,实现更好的视觉效果

  1. 给柱状图添加光照
  2. 给柱状图增加一个底座
  3. 增加一个过渡动画,让柱状图的高度从不显示,到慢慢显示出来。
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>如何实现简单的3D可视化图表:GitHub 贡献图表的3D可视化</title>
        <style>
            #stage {
                width: 840px;
                height: 640px;
                border: 1px dashed #fa8072;
            }
        </style>
    </head>
    <body>
        <script src="https://d3js.org/d3.v5.js"></script>
        <div id="stage"></div>
        <script type="module">
            import { Scene } from 'https://unpkg.com/spritejs/dist/spritejs.esm.js';
            import { Cube, Light, shaders } from 'https://unpkg.com/sprite-extend-3d/dist/sprite-extend-3d.esm.js';
            // 获取该日期之前大约一年的数据
            let cache = null;
            async function getData(toDate = new Date()) {
                if(!cache) {
                    // 先从 JSON 文件中读取数据并缓存起来
                    const data = await (await fetch('./assets/github_contributions_akira-cn.json')).json();
                    cache = data.contributions.map((o) => {
                        o.date = new Date(o.date.replace(/-/g, '/'));
                        return o;
                    });
                }
                // 要拿到 toData 日期之前大约一年的数据(52周)
                let start = 0,
                    end = cache.length;
                // 用二分法查找
                while(start < end - 1) {
                    const mid = Math.floor(0.5 * (start + end));
                    const {date} = cache[mid];
                    if(date <= toDate) end = mid;
                    else start = mid;
                }
                // 获得对应的一年左右的数据
                let day;
                if(end >= cache.length) {
                    day = toDate.getDay();
                } else {
                    const lastItem = cache[end];
                    day = lastItem.date.getDay();
                }
                // 根据当前星期几,再往前拿52周的数据
                const len = 7 * 52 + day + 1;
                const ret = cache.slice(end, end + len);
                if(ret.length < len) {
                    // 日期超过了数据范围,补齐数据
                    const pad = new Array(len - ret.length).fill({count: 0, color: '#ebedf0'});
                    ret.push(...pad);
                }
                return ret;
            }

            (async function () {
                const container = document.getElementById('stage');
                // 创建 Scene 对象
                const scene = new Scene({
                    container,
                    displayRatio: 2, // 设置显示分辨率
                });
                // 创建 Layer 对象:创建了一个 3D(WebGL)上下文的 Canvas 画布
                const layer = scene.layer3d('fglayer', {
                    ambientColor: [0.5, 0.5, 0.5, 1], // 环境光
                    camera: {
                        fov: 35, // 相机的视角设置为 35 度
                    },
                });
                // 相机坐标位置为(6, 6, 6)
                layer.camera.attributes.pos = [6, 6, 6];
                // 相机朝向坐标原点
                layer.camera.lookAt([0, 0, 0]);

                // 添加一道白色的平行光,方向是 (-3, -3, -1)
                const light = new Light({
                    direction: [-3, -3, -1],
                    color: [1, 1, 1, 1]
                });
                layer.addLight(light);

                // 创建用来 3D 展示的 WebGL 程序:shaders.GEOMETRY 默认支持 phong 反射模型的一组着色器
                const program = layer.createProgram({
                    vertex: shaders.GEOMETRY.vertex,
                    fragment: shaders.GEOMETRY.fragment,
                });

                // 获取数据
                const dataset = await getData(new Date(2019, 12, 31));
                const max = d3.max(dataset, (a) => {
                    return a.count;
                });

                // 用数据来操作文档树
                const selection = d3.select(layer);
                /**
                 * 设置长方体 Cube 的属性
                 *      长 (width)
                 *      宽 (depth)
                 *      高 (height)
                 *      y 轴的缩放 (scaleY):设置为 d.count 与 max 的比值,值在 0~1 之间
                 *      位置 (pos)坐标:根据数据的索引设置 x 和 z 来决定的。
                 *      长方体的颜色 (colors)
                 * */
                const chart = selection.selectAll('cube')
                    .data(dataset)
                    .enter()
                    .append(() => {
                        return new Cube(program);
                    })
                    .attr('width', 0.14)
                    .attr('depth', 0.14)
                    .attr('height', 1)
                    .attr('scaleY', 0.001)
                    // .attr('scaleY', (d) => {
                    //     // max 是指一年的提交记录中,提交代码最多那天的数值。
                    //     return d.count / max;
                    // })
                    .attr('pos', (d, i) => {
                        const x0 = -3.8 + 0.0717 + 0.0015;
                        const z0 = -0.5 + 0.05 + 0.0015;
                        const x = x0 + 0.143 * Math.floor(i / 7);
                        const z = z0 + 0.143 * (i % 7);
                        // return [x, 0.5 * d.count /max, z];
                        return [x, 0, z];
                    })
                    .attr('colors', (d, i) => {
                        return d.color;
                    });
                
                layer.setOrbit();

                // 给柱状图增加一个底座
                const fragment = `
                    precision highp float;
                    precision highp int;
                    varying vec4 vColor;
                    varying vec2 vUv;
                    void main() {
                        float x = fract(vUv.x * 53.0);
                        float y = fract(vUv.y * 7.0);
                        x = smoothstep(0.0, 0.1, x) - smoothstep(0.9, 1.0, x);
                        y = smoothstep(0.0, 0.1, y) - smoothstep(0.9, 1.0, y);
                        gl_FragColor = vColor * (x + y);
                    }    
                `;

                const axisProgram = layer.createProgram({
                    vertex: shaders.TEXTURE.vertex,
                    fragment,
                });

                const ground = new Cube(axisProgram, {
                    width: 7.6,
                    height: 0.1,
                    y: -0.049, // not 0.05 to avoid z-fighting
                    depth: 1,
                    colors: 'rgba(0, 0, 0, 0.1)',
                });

                layer.append(ground);

                // 先把 scaleY 直接设为 0.001,然后用 d3.scaleLinear 来创建一个线性的缩放过程
                const linear = d3.scaleLinear()
                    .domain([0, max])
                    .range([0, 1.0]);
                // 最后通过 chart.trainsition 来实现这个线性动画
                chart.transition()
                    .duration(2000)
                    .attr('scaleY', (d, i) => {
                        return linear(d.count);
                    })
                    .attr('y', (d, i) => {
                        return 0.5 * linear(d.count);
                    });
            }());
        </script>
    </body>
</html>

效果如下
在这里插入图片描述

什么是 z-fighting 现象?

在代码里有一处地方需要注意,这这里的 y 不能为 -0.05,我们写成 -0.049,少了 0.001 是为了让上层的柱状图稍微“嵌入”到底座里,从而避免因为底座上部和柱状图底部的 z 坐标一样,导致渲染的时候由于次序问题出现闪烁,这个问题在图形学里叫做 z-fighting

在这里插入图片描述
如果是 y 为 -0.05,就会出现 z-fighting 现象,如下:
在这里插入图片描述

视觉高级篇知识脑图

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/100239.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

黑*头条_第6章_kafka及异步通知文章上下架

黑*头条_第6章_kafka及异步通知文章上下架 文章目录黑*头条_第6章_kafka及异步通知文章上下架1)自媒体文章上下架2)kafka概述3)kafka安装配置4)kafka入门5)kafka高可用设计5.1)集群5.2)备份机制(Replication&#xff09;6)kafka生产者详解6.1)发送类型6.2)参数详解7)kafka消费者…

小林Coding阅读笔记:操作系统篇之硬件结构,CPU Cache一致性问题

前言 参考/导流&#xff1a; 小林coding-2.4 CPU 缓存一致性学习意义 底层基础知识&#xff0c;了解CPU执行过程&#xff0c;让上层编码有效并发控制底层设计思维&#xff08;对比 MySQL的并发控制&#xff09;、更好地去理解JUC的锁、volatile以及JMM架构层面的一致性保证问…

毕业设计 单片机便携式空气质量检测仪 - 物联网 嵌入式

文章目录0 前言1 简介2 主要器件3 实现效果4 设计原理5 部分核心代码5 最后0 前言 &#x1f525; 这两年开始毕业设计和毕业答辩的要求和难度不断提升&#xff0c;传统的毕设题目缺少创新和亮点&#xff0c;往往达不到毕业答辩的要求&#xff0c;这两年不断有学弟学妹告诉学长…

51.`.format`字符串格式化—最适合小白的格式化笔记

51..format字符串格式化 文章目录51..format字符串格式化1.课题导入2.知识回顾3..format的基本用法3.1{}为空3.2 {}中有编号3.3 {}中有变量名4.浮点数的格式化5..format官方语法6. .format格式化语法总结7. 总结8.课堂练习1.课题导入 【描述】 用input函数输入你每个月的工资…

MySQL中WHERE后跟着N多个OR条件会怎样...

GreatSQL社区原创内容未经授权不得随意使用&#xff0c;转载请联系小编并注明来源。GreatSQL是MySQL的国产分支版本&#xff0c;使用上与MySQL一致。作者&#xff1a;叶金荣文章来源&#xff1a;社区原创 可能会执行非常慢&#xff0c;线上生产环境千万别写出这种SQL ... 背景交…

Docker+Gitlab+Jenkins+Springboot

安装Gitlab 开放防火墙端口80和配置映射文件夹 firewall-cmd --zonepublic --add-port80/tcp --permanent firewall-cmd --reload mkdir -p /docker_data/gitlab/{data,logs,config}启动Gitlab容器&#xff08;启动容器之前确保80&#xff0c;443端口没用被占用&#xff0c;被…

H3C双点双向路由引入,以及使用路由策略进行路由控制,路由学习的配置

如下拓扑中&#xff0c;存在两个路由域&#xff0c;左边为isis区域&#xff0c;所有设备均为level-1-2角色。右边为ospf区域&#xff0c;所有使能ospf的接口均在area0区域中&#xff1a; 组网中需要在R1和R3上&#xff0c;分别将各自的isis路由引入到ospf进程中&#xff0c;同时…

Go C 编程 第9课 放飞汽球(魔法学院的奇幻之旅 Go C编程绘图)

Goc编程第八课 Goc编程第八课_哔哩哔哩_bilibili Goc编程第九课 Goc编程第九课_哔哩哔哩_bilibili 59.实心椭圆 (魔法学院第9课) 难度&#xff1a;1 登录 60.双色椭圆 (魔法学院第9课) 难度&#xff1a;1 登录 61.气球串 (魔法学院第9课) 登录 62.同心圆环 (魔法学院第9课…

C++类与对象的应用—日期计算器

目录 一、前言 二、日期类的实现 检查日期的合法性 < 运算符重载 运算符重载 <运算符重载 >运算符重载 >运算符重载 !运算符重载 进一步优化 日期天数 日期天数 日期-天数 日期-天数 前置&&后置 前置--&&后置-- 思路&#…

强化学习的基础知识和6种基本算法解释

强化学习的基础知识和概念简介&#xff08;无模型、在线学习、离线强化学习等&#xff09; 机器学习(ML)分为三个分支:监督学习、无监督学习和强化学习。 监督学习(SL):关注在给定标记训练数据的情况下获得正确的输出无监督学习(UL):关注在没有预先存在的标签的情况下发现数据…

[附源码]Python计算机毕业设计Django校园疫情防范管理系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;我…

我的周刊(第070期)

我的信息周刊&#xff0c;记录这周我看到的有价值的信息&#xff0c;主要针对计算机领域&#xff0c;内容主题极大程度被我个人喜好主导。这个项目核心目的在于记录让自己有印象的信息做一个留存以及共享。&#x1f3af; 项目streamlit[1]用 Python 快速构建数据应用&#xff1…

微信公众号开发—通过网页授权实现业务系统登录及用户绑定(微信网页授权自动登录业务系统)

&#x1f60a; 作者&#xff1a; 一恍过去&#x1f496; 主页&#xff1a; https://blog.csdn.net/zhuocailing3390&#x1f38a; 社区&#xff1a; Java技术栈交流&#x1f389; 主题&#xff1a; 微信公众号开发—通过网页授权实现业务系统登录及用户绑定(微信网页授权自…

产品经理快速入门指南之常见问题篇

产品经理作为近几年互联网最炙手可热的岗位&#xff0c;可谓是赚足了眼球。其实呢&#xff0c;产品经理是很难定义的一个角色&#xff0c;如果非要一句话定义&#xff0c;那么产品经理是为终端用户服务&#xff0c;负责产品整个生命周期的人。今天这篇文章小编会带大家了解一下…

交通部1078-2016版中的音视频协议的一点想法

交通部1078的音视频中的格式有点像TS流, 交通部中1078的音视频的解析用的走的HTTP的协议,内容是流媒体,之前用的是ffmpeg解析的内容流之前是把payload中的数据流区分出音频,视频直接塞给ffmpeg,由ffmpeg推两路流到rtmp协议上去,这种方法有点粗暴, 最近分析ffmpeg源码发现交通…

谷歌通过Chrome简化登录安卓密码支持

谷歌宣布&#xff0c;它正在为其 Chrome 网络浏览器和 Android 操作系统引入密码支持&#xff0c;以简化跨应用程序、网站和设备的登录。 谷歌今天表示&#xff1a; “密码是密码和其他可钓鱼身份验证因素的一种更安全的替代品。它们不能重复使用&#xff0c;不会在服务器漏洞…

AngularJS 2.0 稳定版真的发布了!

导读之前我们还哀叹&#xff0c;谷歌的 AngularJS 2.0 的稳定版看起来年底也未必能见到&#xff0c;然而&#xff0c;在 9 月 14 日谷歌总部召开的一个会议上&#xff0c;突然就宣布最终的稳定版发布了——而这距离前一个版本 RC7 的发布才过去了一天。 AngularJS 2.0 的开发始…

详解入门安全测试最难懂的概念 —— CSRF

对于刚刚入门安全的同学来说&#xff0c;csrf是最难理解的概念之一&#xff0c;本文会用最简单的方式对csrf进行讲解&#xff0c;包括csrf的定义&#xff0c;csrf典型的攻击流程以及如何对其进行防范&#xff0c;希望本文能够帮到大家&#xff01; CSRF定义 CSRF&#xff08;…

Nacos学习笔记 (1)Nacos的简介与安装

1. Nacos 介绍与发展前景 1.1 官网概览&#xff1a; Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service的首字母简称&#xff0c;一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。 Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一…

性能测试(一)—— 概述、策略、指标、流程

目录 一、性能测试概述 1、为什么要进行性能测试&#xff1f; 2、性能测试的概念 2.1 什么是性能&#xff1f; 2.2 什么是性能测试&#xff1f; 2.3 性能测试目的 3、性能测试与功能测试 3.1 焦点不一样 3.2 关系 二、性能测试策略 1、性能测试策略 1.1 基准测试 …