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

[第 46 期] who runs china 可视化 #333

Open
jdk137 opened this issue Mar 5, 2019 · 13 comments
Open

[第 46 期] who runs china 可视化 #333

jdk137 opened this issue Mar 5, 2019 · 13 comments

Comments

@jdk137
Copy link

jdk137 commented Mar 5, 2019

最近忙于开发人大代表数据可视化,可以了解中国决策者们的人群特征和个人公开信息,在国内传媒届反响不错。 https://news.cgtn.com/event/2019/whorunschina/index.html

@ruanyf ruanyf changed the title who runs china 可视化 [第 46 期] who runs china 可视化 Mar 6, 2019
@linnchord
Copy link

well done! 好歹也应该有个中文吧

@Object-oriented
Copy link

哇这也太美了吧,您好请问能讲讲是怎么实现的吗,好想学习呀!

@houyaowei
Copy link

看页面源码是用D3配合插件完成

@jdk137
Copy link
Author

jdk137 commented Mar 7, 2019

实现方式简介

一、展现方式:

分为动画层和交互层。

动画层主要用canvas来渲染粒子的运动。性能较svg优异。

交互层用d3和svg来绘制粒子静止时的图表,绑定交互事件方便,也可以用css来设置交互样式和交互动画。编码方便。

二、流程控制:

主要有两个:页面滚动事件控制以及fsm渲染控制。

1.页面滚动事件控制

页面滚动用到了storytelling神器 scrollama.js 库。
scrollama会触发页面滚动到不同区域时的事件(跨越不同step触发事件单个step内部滚动触发事件
本项目可看startScroll.js

2.自定义的Fsm渲染控制:

fsm有限状态机简单概念可以参考这篇文章
在项目中,页面被分为不同的step, 如性别,年龄等,页面滚动到某个step,fsm就进入一种状态。每个状态包含四个函数:
1). enter 进入某个状态。初始化每个粒子的大小,颜色,速度等
2)update 更新某个状态。如果还在移动阶段,更新每个粒子的位置。如果已经结束,直接返回。
3)endTrans 如果粒子移动已经结束,触发该函数。通常会绘制svg交互层,添加交互层的交互事件等等。另外搜索高亮某个粒子时,也会调用该函数。
4). Leave 退出某个状态。通常会清空svg交互层。
Enter某个状态前,会执行前一个状态的leave函数。update会不断调用直致状态结束。

// 循环更新fsm
    function renderScene(timestamp) {
            fsm.update(timestamp);
        requestAnimationFrame(renderScene);
    }
    renderScene();
    
// fsm 主流程
    fsm = {
        current: null,
        next: null,
        update: function (timestamp) {
            if (fsm.next) {
                var change = function () {
                    fsm.current = fsm.next;
                    fsm.next = null;
                    stages[fsm.current].enter();
                    stages[fsm.current].update(timestamp);
                };
                if (fsm.current) {
                    stages[fsm.current].leave(change);
                } else {
                    change();
                }
            } else if (fsm.current) {
                stages[fsm.current].update(timestamp);
            }
        },
        trans: function (next) {
            if (fsm.current !== next) {
                fsm.next = next;
            }
        },
        endTrans: function () {
            var endTrans = stages[fsm.current].endTrans;
            if (endTrans) {
                endTrans();
            }
        }
    };
    
// fsm状态, 以下为random状态示例
    var stages = window.stages = {
        'random': {
            stage: 'random',
            enter: function () {
                this.isFinished = false;
                this.start = null;
                this.duration = durationTime;
                var stage = this.stage;
                var duration = this.duration;

                var r = 3;
                function getRandomColor() {
                  var letters = '0123456789ABCDEF';
                  var color = '#';
                  for (var i = 0; i < 6; i++) {
                    color += letters[Math.floor(Math.random() * 16)];
                  }
                  return color;
                }
                // compute v
                data.forEach(function (d, i) {
                    var stagePosition = positions[stage];
                    d.position = {
                        startX: d.position.x,
                        startY: d.position.y,
                        endX: stagePosition[i].x,
                        endY: stagePosition[i].y,
                        x: d.position.x,
                        y: d.position.y,
                        vx: (stagePosition[i].x - d.position.x) / duration,
                        vy: (stagePosition[i].y - d.position.y) / duration,
                        r: r,
                        color: d.randomColor || d.position.color || getRandomColor(),
                        index: i
                    };
                    d.randomColor = d.randomColor || d.position.color;
                });
            },
            update: function (timestamp) {
                if (this.isFinished) {
                    return;
                }

                if (!this.start) this.start = timestamp;
                var progress = timestamp - this.start;
                var duration = this.duration;
                // console.log(progress, timestamp, duration, durationTime);
                if (progress >= duration) {
                    this.isFinished = true;
                }

                var setData = function () {
                    data.forEach(function (item, i) {
                        var d = item.position;
                        if (progress >= duration) {
                            d.x = d.endX;
                            d.y = d.endY;
                        } else {
                            d.x = d.startX + d.vx * progress;
                            d.y = d.startY + d.vy * progress;
                        }
                    });
                };

                var draw = function () {
                    ctx.clearRect(0, 0, canvas.width, canvas.height);
                    data.forEach(function (item, i) {
                      var d = item.position;
                      ctx.beginPath();
                      // arc(x, y, radius, startAngle, endAngle, anticlockwise)
                      ctx.arc(d.x, d.y, d.r, 0, 2 * Math.PI, false);
                      ctx.fillStyle = d.color;
                      ctx.fill();
                      ctx.closePath();
                    });
                    if (progress >= duration && (!isMobile.any || recentDeputy !== null)) {
                        stages[fsm.current].endTrans();
                    }
                };

                setData();
                draw();
            },
            endTrans: function () {
                if (!stages[fsm.current].isFinished) {
                    return;
                }
                setTimeout(function () {
                    $mysvg.show();
                    mysvgCircle
                        .attr('cx', function (d, i) { return d.position.x; })
                        .attr('cy', function (d, i) { return d.position.y; })
                        .attr('r', function (d, i) { 
                            var r = recentDeputy === d.index ? 5 : d.position.r;

                            if (recentDeputy === d.index) {
                                console.log(i, d);
                                console.log(r);
                            }
                            return r;
                        })
                        .attr('fill',  function (d, i) { return recentDeputy === d.index ? highlightColor : d.position.color; })
                        .classed('active', function (d) { return recentDeputy === d.index; })
                    if (!isMobile.any) {
                        mysvgCircle.on('mouseover', null);
                        mysvgCircle.on('mouseover', function (d, i) {
                            showPerson(d);
                        });
                        mysvgCircle.on('mouseout', hidePerson);
                    }
                }, 0);

            },
            leave: function (cb) {
                $mysvg.hide();
                cb && cb();
            },
            isFinished: false
        },
        ....
    }

    

三、布局算法

1. 屏幕适配。项目采用了scaleToWindow

这个库可以把一个div缩放,撑满屏幕,效果类似于background 图片的position设置为contain。所以我们只做了PC、移动2种固定的分辨率,然后缩放。极大地简化了布局算法的编写。

2. 文字粒子的定位

我们的设计师很认真地画出了文字粒子的AI图。AI图导出成svg, 就有了各个圆的位置、大小和颜色。通过解析svg文本文件,就得到了文字粒子的定位数据。

3. 常规图表的定位

这个是自己编写的。设计稿中有图例,数字,根据这些图例和数据,手工调整了每一群粒子的位置。

感谢下面的示例,给了不少启发:
https://www.cnblogs.com/hongru/archive/2012/03/28/2420415.html

https://pudding.cool/2017/03/hamilton/

https://flourish.studio/visualisations/survey-data/

@Object-oriented
Copy link

好的,非常感谢大佬的回复,我学习一下。之前我学了canvas,但是用的不多都快忘完了,得好好复习复习了,哈哈。

@rouor
Copy link

rouor commented Mar 7, 2019

so cool, 而且很用心

@Dream4ever
Copy link

well done! 好歹也应该有个中文吧

都干这一行了,要么学英文,要么就安心享用二手的中文资料吧

@jeff-fe
Copy link

jeff-fe commented Mar 8, 2019

做的非常棒,优秀👍

@jeff-fe
Copy link

jeff-fe commented Mar 8, 2019

@jdk137 大佬可以开源代码吗?

@jdk137
Copy link
Author

jdk137 commented Mar 8, 2019

版权不是我的。但网页即源码。

@jackiect
Copy link

jackiect commented Mar 8, 2019

好**酷炫!!

@xiaoxinrui
Copy link

非常棒👍

@lgkill
Copy link

lgkill commented Jun 3, 2019

您好,我想问一下关于安卓端的谷歌浏览器不兼容 scrollama.js 是如何解决的

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants