d3JS:在折线图/面积图上缩小时,绘制大数据集的低密度数据版本



我正在创建一个类似于Mike Bostock的可缩放面积图的图表。

对于我的特定项目,我有一堆传感器,每30秒记录一次值(温度、光线、湿度和声音)。我已经实现了缩放,但当我缩小到一年的比例时,图表的密度会减慢浏览器的速度,图形也无法读取。

如何编辑脚本,使线形图的密度相对于缩放量发生变化?换句话说,x域控制着值线上的点数。当我放大到一小时的时间范围时,我希望有全密度(每30秒录制一次),而当我缩小时,我想要低得多的密度(每天记录一次)。有什么想法吗?使用上面链接中的脚本实现会很有帮助。

谢谢!

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<script type="text/javascript" src="d3/d3.js"></script>
<script type="text/javascript" src="d3/d3.csv.js"></script>
<script type="text/javascript" src="d3/d3.time.js"></script>
<link type="text/css" rel="stylesheet" href="style.css"/>
<style type="text/css">
svg {
font-size: 10px;
}
.axis {
shape-rendering: crispEdges;
}
.axis path, .axis line {
fill: none;
stroke-width: .5px;
}
.x.axis path {
stroke: #000;
}
.x.axis line {
stroke: #fff;
stroke-opacity: .5;
}
.y.axis line {
stroke: #ddd;
}
path.line {
fill: none;
stroke: #000;
stroke-width: .5px;
}
rect.pane {
cursor: move;
fill: none;
pointer-events: all;
}
</style>
</head>
<body>
<div id="body">
<div id="footer">
<span>…</span>
<div class="hint">mousewheel to zoom, drag to pan</div>
</div>
</div>
<script type="text/javascript">
var m = [79, 80, 160, 79],
w = 1280 - m[1] - m[3],
h = 800 - m[0] - m[2],
parse = d3.time.format("%Y-%m-%d").parse,
format = d3.time.format("%Y");
// Scales. Note the inverted domain for the y-scale: bigger is up!
var x = d3.time.scale().range([0, w]),
y = d3.scale.linear().range([h, 0]),
xAxis = d3.svg.axis().scale(x).orient("bottom").tickSize(-h, 0).tickPadding(6),
yAxis = d3.svg.axis().scale(y).orient("right").tickSize(-w).tickPadding(6);
// An area generator.
var area = d3.svg.area()
.interpolate("step-after")
.x(function(d) { return x(d.date); })
.y0(y(0))
.y1(function(d) { return y(d.value); });
// A line generator.
var line = d3.svg.line()
.interpolate("step-after")
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.value); });
var svg = d3.select("body").append("svg:svg")
.attr("width", w + m[1] + m[3])
.attr("height", h + m[0] + m[2])
.append("svg:g")
.attr("transform", "translate(" + m[3] + "," + m[0] + ")");
var gradient = svg.append("svg:defs").append("svg:linearGradient")
.attr("id", "gradient")
.attr("x2", "0%")
.attr("y2", "100%");
gradient.append("svg:stop")
.attr("offset", "0%")
.attr("stop-color", "#fff")
.attr("stop-opacity", .5);
gradient.append("svg:stop")
.attr("offset", "100%")
.attr("stop-color", "#999")
.attr("stop-opacity", 1);
svg.append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("x", x(0))
.attr("y", y(1))
.attr("width", x(1) - x(0))
.attr("height", y(0) - y(1));
svg.append("svg:g")
.attr("class", "y axis")
.attr("transform", "translate(" + w + ",0)");
svg.append("svg:path")
.attr("class", "area")
.attr("clip-path", "url(#clip)")
.style("fill", "url(#gradient)");
svg.append("svg:g")
.attr("class", "x axis")
.attr("transform", "translate(0," + h + ")");
svg.append("svg:path")
.attr("class", "line")
.attr("clip-path", "url(#clip)");
svg.append("svg:rect")
.attr("class", "pane")
.attr("width", w)
.attr("height", h)
.call(d3.behavior.zoom().on("zoom", zoom));
d3.csv("flights-departed.csv", function(data) {
// Parse dates and numbers.
data.forEach(function(d) {
d.date = parse(d.date);
d.value = +d.value;
});
// Compute the maximum price.
x.domain([new Date(1999, 0, 1), new Date(2003, 0, 0)]);
y.domain([0, d3.max(data, function(d) { return d.value; })]);
// Bind the data to our path elements.
svg.select("path.area").data([data]);
svg.select("path.line").data([data]);
draw();
});
function draw() {
svg.select("g.x.axis").call(xAxis);
svg.select("g.y.axis").call(yAxis);
svg.select("path.area").attr("d", area);
svg.select("path.line").attr("d", line);
d3.select("#footer span").text("U.S. Commercial Flights, " + x.domain().map(format).join("-"));
}
function zoom() {
d3.event.transform(x); // TODO d3.behavior.zoom should support extents
draw();
}
</script>
</body>
</html>

Lars和Ari是正确的,这绝对不是一个微不足道的问题。但我认为这是一个重要的问题,对很多人(可能包括我自己)都有用,因此值得花时间去弄清楚。

因此,您可以继续阅读,这是我对Mike Bostock的每日航班计数图形的改编,使其在缩小时(而不是单独的天数)绘制每周/每月/每年平均每日航班计数,并且只绘制可以在任何缩放级别显示的数据子集:
https://jsfiddle.net/ncy5J/2/

以下是我必须做的事情的逐步分解:

  1. 获取非常大的csv数据表,作为JSFiddle脚本中的嵌入变量。我想你不会这样做,但我提到它是因为它很麻烦。在对字符串运行d3.csv.parse()之前,必须在每行的末尾添加一个n

  2. 创建周、月和年的备用数据阵列,并计算这些时间段的日均值:

    • d3.nest与一个键函数一起使用,该键函数使用d3的interval.floor()函数来获取同一年、同一月份等的所有日期以嵌套在一起;

    • 在带有自定义函数的嵌套数组上使用Array.forEach访问嵌套对象的数组,计算其值的平均值,然后将nest()创建的对象替换为与原始数据格式匹配的对象(下面的代码)。

  3. 将数据绑定步骤从初始化移动到重新绘制函数,并更改该函数以接受数据数组作为参数。

  4. 更新d3.behavior.zoom方法以匹配D3版本3 API(原始示例使用v2.4,它有不同的方法将缩放行为链接到缩放比例)。

  5. 将缩放行为调用的zoom函数更改为

    • 从x刻度访问可见数据域(缩放行为会自动更新);

    • 计算该域所覆盖的时间跨度;

    • 从我的四个数据数组中选择一个,该数组将具有该跨度的适当精度级别;

    • 扫描数组以找到可见域中的元素部分(正如我在代码注释中提到的,当你一直放大时,这会有点慢;你也可以使用日期-时间数学来计算数组的正确部分,因为连续元素之间的时间间隔总是相同的)。

    • 使用数组的适当切片作为传入的数据来调用re-draw函数。

下面是步骤2:中的自定义嵌套/平均例程

AllData.yearly = d3.nest().key(function(d){
return d3.time.year.floor(d.date);
})
.entries(data);
AllData.yearly.forEach(meanDaily);
function meanDaily(nestedObject, i, array){
//This function is passed to the Array.forEach() method
//which passes in:
// (1) the element in the array
// (2) the element's index, and
// (3) the array as a whole
//It replaces the element in the array 
//(an object with properties key and values, where
// values is an array of nested objects)
//with a single object that has the nesting key
//value parsed back into a date, 
//and the mean of the nested values' value
//as the value.  It makes sense, I swear.
array[i] = {date:new Date(nestedObject.key), 
value:d3.mean(nestedObject.values, 
function(d) {return d.value;}
)};
}

缩放方法只是基本的javascript,关键是你可以直接从x比例访问可见域,然后用它来确定要传递给draw函数的数据点。

附言:看看不同平均尺度下的数据很有趣。9月11日之后航班的急剧下降在每日、每周和每月的图表上都很突出,但从年度平均值中消失了。相反,年度平均数显示,2002年的日均航班数总体上低于2001年,这提醒人们,在禁飞令解除后很长一段时间,许多人都不敢乘坐飞机。

以下是D3 v6的更新解决方案,运行在ObservableHQ中(并在AmeliaBR早期回答中非常有用的工作的基础上):

  • Mike Bostock的原创作品,显示所有缩放的每日数据:https://observablehq.com/@d3/可缩放面积图
  • 基于缩放级别的动态数据密度:https://observablehq.com/@medmunds/可变数据密度的可缩放面积图

有关所有代码和解释,请参阅ObservableHQ笔记本。一些可能有用的关键部件…

要为当前缩放级别选择最佳数据密度,您可以在缩放的x刻度上找出一天的宽度(以屏幕像素为单位),然后选择最大密度,其中单个时间单位(日/月/周/年)将保持窄于特定像素宽度:

// Return largest of "daily", "weekly", "monthly" or "yearly"
// where a single time unit stays narrower than `thresholdPixels`
// on d3 time scale `x`
function bestDensityForScale(x, thresholdPixels=5) {
const dayWidth = x(Date.UTC(2000, 0, 2)) - x(Date.UTC(2000, 0, 1));
const chooseDensity = d3.scaleThreshold()
.domain([dayWidth * 7, dayWidth * 30, dayWidth * 365])
.range(["daily", "weekly", "monthly", "yearly"]);
return chooseDensity(thresholdPixels);
}

您可能需要调整thresholdPixels以获得所需的效果。把它想象成一个"三角形"的最大宽度;扁平的";低密度图中的剖面,然后将其替换为高密度数据。例如,如果一个月在当前缩放时跨度超过5个像素,则切换到每周数据。如果一周的时间跨度为5个像素,则切换为每天。

为了计算较大时间段的平均每日数据,d3.rollups很有帮助:

const weekly = d3.rollups(
data,
v => d3.mean(v, d => d.value),
d => d3.timeWeek.floor(d.date)
).map(([date, value]) => ({date, value}));

(末尾的maprollups-['2010-01-01', 197062]-返回的数组更改为与日常数据相同的对象格式{date: '2010-01-01', value: 197062}。)

相关内容

最新更新