个性化阅读
专注于IT技术分析

迈向可更新的D3.js图表

点击下载

本文概述

介绍

D3.js是Mike Bostock开发的用于数据可视化的开源库。 D3代表数据驱动的文档, 顾名思义, 该库使开发人员可以轻松地基于数据生成和操作DOM元素。尽管不受库功能的限制, 但D3.js通常与SVG元素一起使用, 并提供了从头开始开发矢量数据可视化的强大工具。

可更新的图表模式可简化D3.js图表

可更新的图表模式可简化D3.js图表

鸣叫

让我们从一个简单的例子开始。假设你正在训练5k竞赛, 并且想制作一张上周每天行驶里程数的水平条形图:

    var milesRun = [2, 5, 4, 1, 2, 6, 5];
 
    d3.select('body').append('svg')
        .attr('height', 300)
        .attr('width', 800)
        .selectAll('rect')
            .data(milesRun)
        	.enter()
            .append('rect')
        	.attr('y', function (d, i) { return i * 40 })
            .attr('height', 35)
        	.attr('x', 0)
       	 .attr('width', function (d) { return d*100})
            .style('fill', 'steelblue');

要查看实际效果, 请在bl.ocks.org上进行查看。

如果这段代码看起来很熟悉, 那就太好了。如果没有, 我发现Scott Murray的教程是D3.js入门的绝佳资源。

作为一个使用D3.js进行了数百小时开发工作的自由职业者, 我的开发模式经历了一次演变, 始终以创造最全面的客户和用户体验为最终目标。正如我稍后将更详细地讨论的那样, Mike Bostock的可重用图表模式为在任何选择中实现相同图表提供了一种可靠的尝试方法。但是, 图表初始化后即可实现其限制。如果我想通过这种方法使用D3的过渡和更新模式, 则必须完全在生成图表的相同范围内处理对数据的更改。实际上, 这意味着在同一功能范围内实现过滤器, 下拉选择, 滑块和调整大小选项。

在亲身经历了这些限制之后, 我想创建一种方法来利用D3.js的全部功能。例如, 侦听完全独立的组件的下拉菜单中的更改, 并无缝触发图表从旧数据到新数据的更新。我希望能够以完整的功能移交图表控件, 并以逻辑和模块化的方式进行移交。结果是一个可更新的图表模式, 我将逐步完成创建该模式的过程。

D3.js图表​​模式进度

步骤1:配置变量

当我开始使用D3.js开发可视化文件时, 使用配置变量快速定义和更改图表规格变得非常方便。这使我的图表可以处理所有不同的长度和数据值。现在, 显示英里运行的同一段代码现在可以显示更长的温度列表, 而不会出现打h的情况:

    var highTemperatures = [77, 71, 82, 87, 84, 78, 80, 84, 86, 72, 71, 68];
    var height = 300;
    var width = 800;
    var barPadding = 1;
    var barSpacing = height / highTemperatures.length;
    var barHeight = barSpacing - barPadding;
    var maxValue = d3.max(highTemperatures);
    var widthScale = width / maxValue;
 
    d3.select('body').append('svg')
            .attr('height', height)
            .attr('width', width)
            .selectAll('rect')
            .data(highTemperatures)
        	.enter()
            .append('rect')
        	.attr('y', function (d, i) { return i * barSpacing })
            .attr('height', barHeight)
        	.attr('x', 0)
            .attr('width', function (d) { return d*widthScale})
            .style('fill', 'steelblue');

要查看实际效果, 请在bl.ocks.org上进行查看。

请注意, 条形的高度和宽度是如何根据数据的大小和值进行缩放的。一个变量被更改, 其余变量被处理。

步骤2:通过函数轻松重复

编码绝对不能复制粘贴

编码绝对不能复制粘贴

鸣叫

通过抽象一些业务逻辑, 我们可以创建更多通用的代码, 这些代码可以处理通用的数据模板。下一步是将此代码包装到生成函数中, 该函数将初始化减少到仅一行。该函数接受三个参数:数据, DOM目标和选项对象, 可用于覆盖默认配置变量。看一下如何做到这一点:

	var milesRun = [2, 5, 4, 1, 2, 6, 5];
	var highTemperatures = [77, 71, 82, 87, 84, 78, 80, 84, 86, 72, 71, 68, 75, 73, 80, 85, 86, 80];
 
	function drawChart(dom, data, options) {
    	var width = options.width || 800;
    	var height = options.height || 200;
    	var barPadding = options.barPadding || 1;
    	var fillColor = options.fillColor || 'steelblue';
 
    	var barSpacing = height / data.length;
    	var barHeight = barSpacing - barPadding;
    	var maxValue = d3.max(data);
    	var widthScale = width / maxValue;
 
    	d3.select(dom).append('svg')
                .attr('height', height)
                .attr('width', width)
                .selectAll('rect')
                .data(data)
                .enter()
                .append('rect')
                .attr('y', function (d, i) { return i * barSpacing })
                .attr('height', barHeight)
                .attr('x', 0)
                .attr('width', function (d) { return d*widthScale})
                .style('fill', fillColor);
    	}
 
    	var weatherOptions = {fillColor: 'coral'};
    	drawChart('#weatherHistory', highTemperatures, weatherOptions);
 
    	var runningOptions = {barPadding: 2};
    	drawChart('#runningHistory', milesRun, runningOptions);

要查看实际效果, 请在bl.ocks.org上进行查看。

在此情况下, 请注意D3.js的选择, 这一点也很重要。应始终避免使用一般选择, 例如d3.selectAll(‘rect’)。如果SVG位于页面其他位置, 则页面上的所有矩形都将成为选择的一部分。而是使用传入的DOM引用创建一个svg对象, 在添加和更新元素时​​可以引用该对象。该技术还可以改善图表生成的运行时间, 因为使用类似条的引用也可以避免再次选择D3.js。

步骤3:方法链接和选择

虽然以前的使用配置对象的框架在JavaScript库中非常普遍, 但D3.js的创建者Mike Bostock建议使用另一种模式来创建可重复使用的图表。简而言之, Mike Bostock建议使用getter-setter方法将图表实现为闭包。在给图表实现增加一些复杂性的同时, 通过简单地使用方法链接, 设置配置选项对于调用者来说变得非常简单:

	// Using Mike Bostock's Towards Reusable Charts Pattern
	function barChart() {
 
    	// All options that should be accessible to caller
    	var width = 900;
    	var height = 200;
    	var barPadding = 1;
    	var fillColor = 'steelblue';
 
    	function chart(selection){
            selection.each(function (data) {
            	var barSpacing = height / data.length;
            	var barHeight = barSpacing - barPadding;
            	var maxValue = d3.max(data);
            	var widthScale = width / maxValue;
 
                d3.select(this).append('svg')
                    .attr('height', height)
                    .attr('width', width)
                    .selectAll('rect')
                    .data(data)
                	.enter()
                    .append('rect')
                    .attr('y', function (d, i) { return i * barSpacing })
                    .attr('height', barHeight)
                    .attr('x', 0)
                    .attr('width', function (d) { return d*widthScale})
                    .style('fill', fillColor);
        	});
    	}
 
    	chart.width = function(value) {
        	if (!arguments.length) return margin;
        	width = value;
        	return chart;
    	};
 
    	chart.height = function(value) {
        	if (!arguments.length) return height;
        	height = value;
        	return chart;
    	};
 
        chart.barPadding = function(value) {
        	if (!arguments.length) return barPadding;
        	barPadding = value;
        	return chart;
    	};
 
        chart.fillColor = function(value) {
        	if (!arguments.length) return fillColor;
        	fillColor = value;
        	return chart;
    	};
 
    	return chart;
	}	
	var milesRun = [2, 5, 4, 1, 2, 6, 5];
	var highTemperatures = [77, 71, 82, 87, 84, 78, 80, 84, 86, 72, 71, 68, 75, 73, 80, 85, 86, 80];
 
	var runningChart = barChart().barPadding(2);
    d3.select('#runningHistory')
            .datum(milesRun)
            .call(runningChart);
 
	var weatherChart = barChart().fillColor('coral');
    d3.select('#weatherHistory')
            .datum(highTemperatures)
            .call(weatherChart);

要查看实际效果, 请在bl.ocks.org上进行查看。

图表初始化使用D3.js选择, 绑定相关数据, 并将DOM选择作为此上下文传递给生成器函数。生成器函数将默认变量包装在一个闭包中, 并允许调用方通过与返回图表对象的配置函数的方法链接来更改这些默认变量。通过这样做, 调用者可以一次将同一图表呈现给多个选择, 或者使用一个图表将同一图表呈现给具有不同数据的不同选择, 同时避免绕过笨重的选项对象。

步骤4:可更新图表的新模式

Mike Bostock建议的先前模式为图表开发人员提供了生成器功能内的强大功能。给定一组数据和传入的任何链式配置, 我们可以从那里控制一切。如果需要从内部更改数据, 我们可以使用适当的过渡方式, 而不仅仅是重新绘制。即使是诸如窗口大小调整之类的事情也可以轻松处理, 创建响应功能, 例如使用缩写文本或更改轴标签。

但是, 如果从生成器功能范围之外修改数据怎么办?或者, 如果需要以编程方式调整图表大小, 该怎么办?我们可以再次调用带有新数据和新大小配置的图表函数。一切都将被重画, 并且瞧瞧。问题解决了。

不幸的是, 这种解决方案存在许多问题。

首先, 我们几乎不可避免地要执行不必要的初始化计算。当我们要做的只是缩放宽度时, 为什么要进行复杂的数据处理?第一次初始化图表时可能需要进行这些计算, 但是当然不必在我们需要进行的每次更新中进行。每个程序化请求都需要进行一些修改, 作为开发人员, 我们确切地知道这些更改是什么。不多不少。此外, 在图表范围内, 我们已经可以访问许多需要的内容(SVG对象, 当前数据状态等), 从而使更改易于实现。

以上面的条形图为例。如果我们想更新宽度, 并通过重绘整个图表来完成, 我们将触发许多不必要的计算:找到最大数据值, 计算钢筋高度以及渲染所有这些SVG元素。确实, 将width分配给它的新值后, 我们要做的唯一更改是:

width = newWidth;
widthScale = width / maxValue;
bars.attr('width', function(d) { return d*widthScale});
svg.attr('width', width);

但它变得更好。由于我们现在具有图表的一些历史记录, 因此我们可以使用D3的内置过渡来更新图表并轻松对其进行动画处理。继续上面的示例, 添加宽度上的过渡就像更改一样简单

bars.attr('width', function(d) { return d*widthScale});

to

bars.transition().duration(1000).attr('width', function(d) { return d*widthScale});

更好的是, 如果我们允许用户传入新数据集, 则可以使用D3的更新选择(输入, 更新和退出)将过渡应用于新数据。但是我们如何允许新数据呢?回想一下, 我们以前的实现创建了一个新的图表, 如下所示:

d3.select('#weatherHistory')
    .datum(highTemperatures)
	.call(weatherChart);

我们将数据绑定到D3.js选择, 并称为可重用图表。数据的任何更改都必须通过将新数据绑定到同一选择来完成。从理论上讲, 我们可以使用旧模式并探测现有数据的选择, 然后使用新数据更新我们的发现。这不仅麻烦而且实施起来很复杂, 而且还需要假设现有图表具有相同的类型和形式。

相反, 通过对JavaScript生成器函数的结构进行一些更改, 我们可以创建一个图表, 使调用者可以通过方法链轻松地从外部提示更改。尽管在设置配置和数据之前, 然后保持不变, 但是即使在初始化图表之后, 调用方现在仍可以执行以下操作:

weatherChart.width(420);

结果是从现有图表平滑过渡到新宽度。无需进行不必要的计算和平滑的过渡, 结果就是满意的客户。

没有不必要的计算+流畅的过渡=客户满意

没有不必要的计算+流畅的过渡=客户满意

鸣叫

这种额外的功能会稍微增加开发人员的工作量。但是, 从历史上看, 我发现付出的努力是值得的。这是可更新图表的框架:

function barChart() {
 
	// All options that should be accessible to caller
	var data = [];
	var width = 800;
	//... the rest
 
	var updateData;
	var updateWidth;
	//... the rest
 
	function chart(selection){
        selection.each(function () {
 
        	//
        	//draw the chart here using data, width
        	//
 
            updateWidth = function() {
            	// use width to make any changes
        	};
 
        	updateData = function() {
            	// use D3 update pattern with data
        	}
 
    	});
	}
 
	chart.data = function(value) {
    	if (!arguments.length) return data;
    	data = value;
    	if (typeof updateData === 'function') updateData();
    	return chart;
	};
 
	chart.width = function(value) {
    	if (!arguments.length) return width;
    	width = value;
    	if (typeof updateWidth === 'function') updateWidth();
    	return chart;
	};
	//... the rest
 
	return chart;
}

要查看已完全实施, 请在bl.ocks.org上进行检查。

让我们回顾一下新结构。与以前的闭包实现相比, 最大的变化是增加了更新功能。如前所述, 这些功能利用D3.js转换和更新模式来根据新数据或图表配置平稳地进行任何必要的更改。为了使调用者可以访问它们, 将函数作为属性添加到图表中。为了使操作更轻松, 初始配置和更新都通过相同的功能进行处理:

chart.width = function(value) {
    if (!arguments.length) return width;
    width = value;
    if (typeof updateWidth === 'function') updateWidth();
    return chart;
};

请注意, 在图表初始化之前, 不会定义updateWidth。如果未定义, 则将全局设置配置变量并在图表关闭中使用。如果已经调用了图表函数, 则所有转换都将移交给updateWidth函数, 该函数使用更改后的width变量进行所需的更改。像这样:

updateWidth = function() {
    widthScale = width / maxValue;
    bars.transition().duration(1000).attr('width', function(d) { return d*widthScale});
    svg.transition().duration(1000).attr('width', width);
};

有了这种新结构, 就可以像其他配置变量一样, 通过方法链接来传递图表数据, 而不是将其绑定到D3.js选择中。区别:

var weatherChart = barChart();
 
d3.select('#weatherHistory')
       .datum(highTemperatures)
       .call(weatherChart);

变成:

var weatherChart = barChart().data(highTemperatures);
 
d3.select('#weatherHistory')
         .call(weatherChart);

因此, 我们进行了一些更改, 并增加了一些开发人员的精力, 让我们看看其中的好处。

假设你有一个新功能请求:”添加一个下拉菜单, 以便用户可以在高温和低温之间切换。并在你使用时也改变颜色。”现在, 你可以在选择低温时进行一个简单的调用, 而无需清除当前图表, 绑定新数据并从头开始重画, 所以:

weatherChart.data(lowTemperatures).fillColor(‘blue’);

享受魔术。我们不仅可以保存计算, 还可以在可视化文件更新时为其添加新的理解水平, 这是以前无法实现的。

这里需要有关转换的重要警告。在同一元素上安排多个转换时要小心。开始新的过渡将隐式取消任何先前正在运行的过渡。当然, 可以在一个D3.js启动的过渡中的一个元素上更改多个属性或样式, 但是我遇到了一些实例, 其中同时触发了多个过渡。在这些情况下, 创建更新函数时, 请考虑在父元素和子元素上使用并发过渡。

哲学的改变

Mike Bostock引入了闭包作为封装图表生成的一种方法。他的模式经过优化, 可以在许多地方使用不同的数据创建相同的图表。但是, 在使用D3.js的几年中, 我发现优先级略有不同。我介绍的新模式无需使用图表的一个实例来创建具有不同数据的相同可视化文件, 而是使调用者可以轻松地创建图表的多个实例, 即使在初始化后也可以完全修改每个实例。此外, 可以通过完全访问图表的当前状态来处理每个更新, 从而使开发人员可以消除不必要的计算, 并利用D3.js的功能来创建更无缝的用户和客户端体验。

赞(0)
未经允许不得转载:srcmini » 迈向可更新的D3.js图表

评论 抢沙发

评论前必须登录!