65.9K
CodeProject 正在变化。 阅读更多。
Home

基于 d3.js 构建人口统计数据可视化工具

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (5投票s)

2016年4月3日

CPOL

7分钟阅读

viewsIcon

30807

在本文中,我们将基于从 REST API 获取的人口统计数据,创建一个类似于 Gapminder 的数据可视化工具。

引言

如果您对数据分析和数据可视化感兴趣,可能已经遇到过一个名为 Gapminder 的工具。Gapminder 旨在提供统计数据(主要来自联合国组织)并将其与漂亮的动画功能相结合。Gapminder 的发起人是瑞典教授 Hans Rosling,他有一个(几乎是传奇的)TED 演讲,展示了 Gapminder,如果您还没看过,绝对值得一看。

Gapminder

Gapminder 最初于 2005 年实现,它有一些当时并不容易实现(至少用 JavaScript 不容易)的漂亮功能。幸运的是,如今许多出色的开源 JavaScript 库让 Web 开发人员的生活变得轻松快捷。这也是本教程的目的:一个概念验证,展示了当我们拥有正确的库和 API 时,构建数据可视化工具是多么容易。因此,本教程可能侧重于人口统计数据可视化,但一旦概念清晰,它可以简单地修改用于任何类型的数据可视化。但现在,让我们开始构建一个类似于 Gapminder 的在线工具吧!

开始之前...

请确保您对 JavaScript、jQuery 有基本了解,以及 REST 接口的工作原理和如何读取 JSON 数据对象。这些是本教程的先决条件。您可以在这里找到一个关于 jQuery 的精彩教程,关于 REST 的一些基本信息可以在这里找到。

获取数据

在人口统计数据可视化方面,最重要的问题之一是如何获取数据。有成千上万个来自不同组织提供数据的 API,本教程我们将使用一个名为 INQStats 的服务。我使用这个 API 有几个原因:

  1. 免费使用,没有任何限制
  2. 它从不同的可信来源(例如联合国数据)收集人口统计数据,并以统一的格式返回
  3. 它使用 REST API 并返回 JSON 数据——这使得在我们的 JavaScript/AJAX 实现中易于使用
  4. 我是作者,所以我知道幕后发生了什么,如果需要,我可以根据我的需求调整 API。

如果您不想使用 INQStats,您可以自由选择替代 API。查看世界银行的 API联合国的 API

使用 INQStats 获取数据非常简单:只需在此处注册一个 API 密钥(您会立即获得),然后开始发出请求。例如,如果您想获取澳大利亚过去五年的人口数据,请在浏览器地址栏中输入以下请求并按回车键(不要忘记用您自己的密钥替换 api_key 参数)

http://inqstatsapi.inqubu.com/?api_key=ADDKEYHERE&data=population&countries=au

将返回以下代码

[
  {
    "countryCode":"au",
    "countryName":"Australia",
    "population":[
	  {
	    "year":"2014",
	    "data":"23490736"
	  },{
	    "year":"2013",
	    "data":"23130900"
	  },{
	    "year":"2012",
	    "data":"22683600"
	  },{
	    "year":"2011",
	    "data":"22323900"
	  },{
	    "year":"2010",
	    "data":"22065300"
	  }
	]
  }
]

这在我们只需要特定国家的数据时很好,但实际上,我们希望比较世界上所有国家。幸运的是,这也是可能的。如果您提交以下 HTTP 请求,将返回一个 JSON 对象数组,其中包含世界上所有国家和 2014 年的人均国内生产总值

http://inqstatsapi.inqubu.com/?api_key=ADDKEYHERE&cmd=getWorldData&data=gdp_capita&year=2014&addRegion=true

注意:如果您不添加 year 参数,将返回最新数据。

我们现在可以编写一个简单的函数,该函数将两个数据键(x 轴和 y 轴)和年份作为输入参数,返回包含所述两个数据键和国家人口的所有国家列表,并将其保存在名为 dataset 的变量中。使用 jQuery,这非常简单

var dataset = [];
function getData(key1, key2, year) {
	$.ajax({
		url: 'http://inqstatsapi.inqubu.com?api_key=ADDKEYHERE&cmd=getWorldData&data=' + 
		key1 + ',' + key2 + ',population&year=' + year + '&addRegion=true',
		complete: function(response) {
			dataset = JSON.parse(response.responseText);
                },
                error: function(e) {
                    alert("Data could not be loaded from URI\n" + e.statusText);
                },
	});
	return false;
}

让我们将它可视化!

纯数据本身很无聊,所以让我们制作一些精美的图形来给文本内容带来活力!JavaScript 中最强大的库之一是 d3.js。当我们想要绘制和操作图表时,D3.js 会提供支持,它非常灵活且易于定制——这就是 d3.js 目前成为网络数据可视化最受欢迎的框架之一的原因。

最终结果应该是这样的:

Data Visualizer

可以在这里找到实时演示和源代码。我们现在将介绍代码中最相关的部分,看看发生了什么!图表本身将放置在一个容器 <div id="container"></div> 中。要将 SVG 图表添加到此容器,我们使用

var svg = d3.select("#container").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
    .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

D3 有自己的选择器函数 d3.select()。如果传入的 string# 开头,则会选择具有特定 ID 的元素。之后,将一个 svg 标签添加到容器中,并多次调用 .attr() 函数。.attr() 功能非常强大,可以以不同的方式使用:第一个参数说明更改应应用于哪个对象或变量,第二个参数说明更改的样式。如果第二个参数是一个函数,则更改将应用于所有选定的元素。在我们的例子中,它更简单:我们在这里设置图表的宽度、高度和边距。(注意:当然,宽度、高度和边距必须首先声明)。此外,通过 .append("g"),我们向 SVG 添加一个 <g> 标签——这用于形状分组。

您可能已经注意到,d3 大量使用了方法链。这意味着,通过连接函数,前一个函数的结果可以平滑地传递给下一个函数。

接下来,我们看看 getData 函数

function getData(key1, key2, year) {
    $.ajax({
        url: 'http://inqstatsapi.inqubu.com?api_key=ADDKEYHERE&cmd=getWorldData&data=' + 
        key1 + ',' + key2 + ',population&year=' + year + '&addRegion=true',
        complete: function(response) {
            var dataObj = JSON.parse(response.responseText);
            if (dataObj.type != undefined && dataObj.type == "error") {
                alert("Error: " + dataObj.msg)
            } else {
                dataset = dataObj;
                update();
            }
        },
        error: function(e) {
            alert("Data could not be loaded from URI\n" + e.statusText);
        },
    });
    return false;
}

我们重用了上面的代码,并首先添加了一些错误处理:当从 INQStats API 返回的 JSON 对象包含一个名为 type 的变量且值为“error”时,我们知道在尝试获取数据时出了问题。因此,我们将通过一个简单的警报消息框显示 msg 中的错误消息。如果一切顺利,我们将收到的数据保存在一个名为 dataset 的变量中,并调用 update() 函数。

update 函数的第一部分,我们将 dataset 中的所有值解析为数字

dataset.forEach(function(d) {
    d[xValue] = +d[xValue];
    d[yValue] = +d[yValue];
});

在这种情况下,d 表示当前对象(我们数据集中数组中的当前国家对象),xValue 是当前选定的 x 轴数据键,y 轴也是如此。之后,我们更新轴

xScale.domain([d3.min(dataset, function(d) {
    return d[xValue];
}), d3.max(dataset, function(d) {
    return d[xValue];
})]);

yScale.domain([d3.min(dataset, function(d) {
    return d[yValue];
}), d3.max(dataset, function(d) {
    return d[yValue];
})]);

xScale 表示 x 轴的线性刻度。可以通过调用 .domain() 函数来修改轴的范围。第一个参数是最小值,第二个参数是最大值。使用 d3.min(),我们获取 dataset 中的最小值,使用 d3.max() 获取最大值。

接下来,我们要绘制我们的点。首先,我们选择 SVG 图形中的所有 circle 元素(它们尚未添加,但我们稍后会讲到),并将 dataset 绑定到选择。然后,我们使用 .enter() 结合 .append() 在选择上添加那些未在选择中呈现的新圆形元素(因为没有,所以所有圆形都是新创建的)。之后,我们用 cxcy 设置点的坐标,并用 r 设置半径。我们使大小取决于一个国家的人口,所以:人口越多,点越大。我们还将每个圆形元素的 ID 设置为国家的名称。

var circles = svg.selectAll("circle").data(dataset);

circles.enter().append("circle")
    .attr({
        cx: function(d) {
            return xScale(d[xValue]);
        },
        cy: function(d) {
            return yScale(d[yValue]);
        },
        r: function(d) {
            if (d.population < 1000000) {
                return 2;
            }
            [...]
        },
        id: function(d) {
            return d.countryName;
        }
    })
    .style("stroke", "black")
    .style("fill", function(d) {
        return color(d.region);
    });

添加滑块和播放按钮以可视化年度变化

Gapminder 最引人注目的功能是数据变化依赖于年份的动画。我们正在添加类似的功能,一个用于年份选择的滑块和一个使滑块逐年向前跳的播放按钮。当点击 <button name="play" id="play">Play</button> 时触发的函数

var running = false;
var timer;
$("#slider").value = 2010;
$("#play").on("click", function() {
    var maxstep = 2015,
        minstep = 2009;
    if (running == true) {
        $("#play").html("Play");
        running = false;
        clearInterval(timer);
    } else if (running == false) {
        $("#play").html("Pause");
        sliderValue = $("#slider").val();
        timer = setInterval(function() {
            if (sliderValue >= maxstep) {
                clearInterval(timer);
                return;
            }
            sliderValue++;
            getData(xValue, yValue, sliderValue);
            setTimeout(function() {
                $("#slider").val(sliderValue);
                $('#range').html(sliderValue);
                $("#slider").val(sliderValue);
                year = $("#slider").val();

            }, 500)
        }, 3000);
        running = true;
    }
});

当当前没有动画运行时,会创建一个新的间隔,并每 3 秒调用一次 getData()sliderValue 是年份,并一直计数直到达到最大年份(2015)。在调用数据函数和更新 DOM 中的值之间有 0.5 秒的延迟。这样做的原因是补偿发送请求和获取数据之间的延迟,并使其对用户来说看起来更“实时”。

结论

本教程的目的是展示如今构建一个简单的数据可视化工具是多么容易和快速。希望所提供的信息对您有用,如果有什么不清楚、错误或有其他问题,请留言。谢谢您,祝一切顺利!

© . All rights reserved.