流式数据可视化:从心电图到随机流图
在当今数据驱动的时代,流式数据的可视化变得至关重要。它能够帮助我们实时理解和分析不断变化的数据。本文将详细介绍如何实现流式数据的可视化,包括搭建WebSocket服务器、创建心电图和呼吸信息的可视化,以及生成随机数据驱动的流图。
1. 搭建WebSocket服务器
我们使用一个简单的Node.js脚本来搭建WebSocket服务器。以下是具体步骤:
1.
安装依赖
:移动到
<DVD3>/chapter-06/bin
目录,运行
npm install
命令,这将安装我们要使用的WebSocket库。
2.
启动服务器
:在该目录下,有一个名为
server-hr.js
的脚本,可用于启动WebSocket服务器。这个简单的服务器接受几个参数:
-
fileName
:第一个参数是使用
rdsamp
命令创建的文件。
-
sendInterval
:指定发送数据的频率。如果需要实时表示,应使用4ms的
sendInterval
(即250Hz)。
-
toSkip
:设置要跳过的记录数。例如,如果设置
sendInterval
为8ms,则应跳过每隔一条记录,以确保前端接收到的数据具有正确的时间尺度。
以下是服务器的代码:
var fs = require('fs')
var ws = require('ws')
if (process.argv.length != 5) {
console.log("Please specify the data to stream ," +
" the send interval and the records to skip as arguments")
process.exit(1)
}
var sendInterval = +process.argv[3]
var toSkip = +process.argv[4]
fs.readFile(process.argv[2], 'utf8', function (err,data) {
if (err) {
console.log("Error loading file", err); process.exit(1)
}
startupServer(data.split('\n'))
});
function startupServer(data) {
// convert the data into a simple json structure
var processed = data.map(function(el) {
var splitted = el.trim().split(/\s+/);
return {
"id" : splitted[0], "resp" : +splitted[1], "ecg" : +splitted[2]
}
})
var WebSocketServer = ws.Server;
var wss = new WebSocketServer({ port: 8081 });
function broadcast() {
var skipped = processed.splice(0, toSkip);
var toSend = processed.shift();
wss.clients.forEach(function each(client) {
client.send(JSON.stringify(toSend), {}, function(cb) {});
});
skipped.forEach(function(el) {processed.push(el)})
processed.push(toSend)
};
setInterval(function() { broadcast(); }, sendInterval);
}
这个脚本读取我们作为命令行参数提供的数据文件(
fs.readFile
)。加载后,将数据分割成段并存储在
processed
数组中。然后,启动一个监听端口8081的WebSocket服务器。还定义了一个名为
broadcast
的函数,它根据
sendInterval
将一个数据元素作为JSON字符串发送给所有连接的监听器,并在发送后将数据推到数组的末尾。因此,即使我们只有1分钟的数据,也可以无限期地运行这个服务器。
启动服务器的命令如下:
node server-hr.js ../data/yng.csv 16 3
此时,我们每隔16ms发送一条消息,因此每发送一条消息应跳过三条记录。当连接到这个WebSocket时,我们将收到包含
id
、
resp
字段和
ecg
字段的记录。
2. 创建心电图和呼吸信息可视化
可视化将具有以下特点:
- 绘制一条显示心电图和呼吸信息的线。
- 加载两个外部SVG图像(心脏和一组肺),并根据接收到的数据对其进行动画处理。
2.1 初始化数据结构和比例尺
首先,我们需要定义一些变量:
var ecgAvg = 16800;
var respAvg = 16000;
var n = 800;
var data = d3.range(n).map(function(d) {return {
"ecg": ecgAvg,
"resp": respAvg
}});
我们使用
ecgAvg
和
respAvg
来预填充一个数组,以便在新数据到来时以空线开始可视化。
n
变量确定每条线要渲染的点数。这个数字越低,线就越不紧凑。
接下来,定义一些比例尺:
var x = d3.scaleLinear().domain([0, n - 1]).range([100, width]);
var yEcg = d3.scaleLinear().domain([15800, 26000]).range([height/2, 0]);
var yResp = d3.scaleLinear().domain([14800, 24000]).range([height/2 + height/2, 0]);
var sEcg = d3.scaleLog().domain([15800, 26000]).range([1, 0]);
var sResp = d3.scaleLinear().domain([14800, 24000]).range([1, 0]);
这些是相当标准的比例尺。
x
比例尺用于确定线中各点的x位置,
yEcg
和
yResp
将确定各点的y位置。
sEcg
和
sResp
分别控制心脏SVG图像和肺SVG图像的不透明度。
由于我们要绘制线,需要几个
d3.line
生成器:
var lineEcg = d3.line()
.x(function(d, i) { return x(i); })
.y(function(d) { return yEcg(+d.ecg); });
var lineResp = d3.line()
.x(function(d, i) { return x(i); })
.y(function(d) { return yResp(+d.resp); });
注意,我们没有在这些元素上指定专用的曲线函数,因为我们绘制了很多小的点(
n = 800
),线本身已经很平滑,不需要额外的插值。
现在可以绘制线了:
svg.append("g").append("path").datum(data)
.attr("class", "ecg")
.attr("d", lineEcg);
svg.append("g").append("path").datum(data)
.attr("class", "resp")
.attr("d", lineResp);
此时,这只是一条简单的线,因为我们将数据数组的所有元素初始化为相同的值。
2.2 加载图像并建立WebSocket连接
使用
d3.queue
调用
d3.xml
来加载图像:
d3.queue()
.defer(d3.xml, 'data/heart.svg')
.defer(d3.xml, 'data/lungs.svg')
.await(start)
加载后,将图像添加到SVG元素中,并进行一些缩放,使其大小相同:
function start(err, heart, lungs) {
var addedHeart =
svg.append("g").attr("class","heartContainer").node()
.appendChild(heart.documentElement.querySelector("g"))
d3.select(addedHeart).attr("class","heart")
.attr("transform", "translate(0 " + (yEcg(ecgAvg)-30) + " ) scale(0.1 0.1)")
var addedLungs = svg.append("g").attr("class",
"lungContainer").node()
.appendChild(lungs.documentElement.querySelector("g"))
d3.select(addedLungs).attr("class","lungs")
.attr("transform", "translate( 0 " + (yResp(respAvg)-30) + " ) scale(2 2)");
...
}
添加外部SVG图像通常比较麻烦,因为它们通常嵌入在一个SVG根元素中,这使得转换它们变得更加困难。我们只选择文件中的第一个
g
元素并将其添加到我们自己的组中,这样就可以轻松定义自己的转换属性。
建立WebSocket连接:
function start(err, heart, lungs) {
...
var connection = new WebSocket('ws://localhost:8081');
connection.onerror = function (error) {
console.log('WebSocket Error ' + error);
};
connection.onmessage = function (e) {
process(JSON.parse(e.data));
};
}
当服务器推送消息时,
connection.onmessage
函数将被调用,将消息转换为JavaScript对象并传递给
process
函数进行处理。
2.3 处理服务器更新
function process(received) {
// Push a new data point onto the back.
data.push(received);
d3.select(".ecg").attr("d", lineEcg);
d3.select(".resp").attr("d", lineResp);
d3.select(".heartContainer").attr("opacity", sEcg(+received.ecg))
d3.select(".lungContainer").attr("opacity", sResp(+received.resp))
data.shift();
}
每次接收到事件时,将其添加到数组末尾并重新绘制所有内容。由于我们指定了800个数据点,因此会得到一个平滑流动的图形。重新绘制图像后,从数据数组中移除第一个元素,使图形向左移动一步。
例如,当我们以
node server-hr.js ../data/yng.csv 80 19
的方式运行服务器时,大约每秒绘制12次线。虽然线条的细节水平,尤其是心电图的细节水平不是很高,但使用较低的更新间隔的优点是我们只需要每秒重绘几次图像,而不是250次。
3. 随机数据驱动的流图
流图是一种渲染面积图的替代方式,不同系列的数据相互叠加,底部可以波动,提供了一种视觉上吸引人的方式来显示不同系列的数据。
3.1 随机数据WebSocket服务器
这个WebSocket服务器与前面的类似,但这次它在特定间隔生成一组随机数据:
var fs = require('fs')
var ws = require('ws')
if (process.argv.length != 4) {
console.log("Please specify the number of streams and the interval as arguments.")
process.exit(1)
}
var n = +process.argv[2];
var sendInterval = +process.argv[3]
startupServer(n)
function startupServer(n) {
var WebSocketServer = ws.Server;
var wss = new WebSocketServer({ port: 8081 });
var count = 1;
wss.on('connection', function connection(ws) {
console.log('received connection');
});
function broadcast() {
var data = { n: n };
for (var i = 0 ; i < n ; i++) {
data[i] = Math.random()*Math.random()*Math.random();
}
wss.clients.forEach(function each(client) {
client.send(JSON.stringify(data), {}, function(cb) {});
});
count++;
};
setInterval(function() { broadcast(); }, sendInterval);
}
这个脚本接受两个参数:第一个参数是要生成的数据元素数量,第二个参数指定将数据推送到客户端的间隔。启动服务器的命令如下:
$ node server-random.js 7 100
连接WebSocket客户端时,将收到一个包含七个随机值的JSON对象。
3.2 创建流图
创建流图需要以下步骤:
1.
设置比例尺和生成器
:
var totalDatapoints = 20; // number of points used to draw the streamgraph
var numberSeries = 5; // number of series to show
var interval = 1000; // interval at which to rerender
var x = d3.scaleLinear()
.domain([0, totalDatapoints-3])
.range([0, width]);
var y = d3.scaleLinear().domain([-2, 5]).range([height, 0]);
var color = d3.scaleLinear().domain([0, numberOfSeries-1]).range(["red", "orange"]);
var area = d3.area().curve(d3.curveNatural)
.x(function(d,i) { return x(i-1); })
.y0(function(d) { return y(d[1]); })
.y1(function(d) { return y(d[0]); });
totalDatapoints
确定构成流图中一条线的控制点数量,
numberSeries
定义要显示的区域数量,
interval
确定多久添加一个新点并重新绘制图形。
x
比例尺用于确定x轴位置,
y
比例尺用于确定y轴位置,
color
比例尺将系列映射到从红色到橙色的颜色范围,
area
生成器用于绘制区域。
2.
定义数据和过渡
:
var data = initEmpty(totalDatapoints, numberSeries);
var stack = d3.stack().offset(d3.stackOffsetWiggle)
.keys(d3.range(numberSeries).map(function(d) {return d}));
function initEmpty(totalDatapoints, numberSeries) {
return d3.range(totalDatapoints).map(function(nc) {
return d3.range(numberSeries).reduce(function(res, mc) {
res[mc] = 0.7; return res;
}, {id: nc});
})
}
初始化数据数组,并使用
d3.stack
函数准备数据,以便与
d3.area
生成器一起使用。
d3.stack
函数中的
offset
参数可以配置为将数据渲染为流图而不是普通面积图。有两种选项:
-
d3.stackOffsetSilhouette
:将流图的基线设置为中心为零,即使添加新数据也保持不变。
-
d3.stackOffsetWiggle
:基线根据系列的加权摆动设置,当数据添加或删除时基线可以移动。我们在示例中使用了这个选项。
连接到随机数据生成的WebSocket服务器:
var receivedData = {};
var isRunning = false;
var connection = new WebSocket('ws://localhost:8081');
connection.onmessage = function (received) {
var frame = JSON.parse(received.data);
Object.keys(frame).forEach(function(key) {
if (!receivedData[key]) {
receivedData[key] = 0;
}
receivedData[key] += +frame[key];
})
// kick off processing.
if (!isRunning) { isRunning = true; render(); }
};
每次收到数据时,将其添加到
receivedData
对象中,并在第一次收到数据时启动渲染。
渲染函数如下:
function render() {
data.push(receivedData);
var stacked = stack(data)
var existingEls = g.selectAll("path").data(stacked)
var newEls = existingEls.enter().append("path")
.style("fill", function(d,i) { return color(i); });
receivedData = {}
var all = existingEls.merge(newEls)
.attr("transform", null)
.transition().duration(interval).ease(d3.easeLinear)
.attrTween("d", function(d, i) {
var oldData = this._old
? this._old
: d3.range(totalDatapoints + 1).map(function() {return {'0': 0, '1': 0}});
var currentData = d;
var interpolator = d3.interpolate(oldData, currentData)
return function(t) {
return area(interpolator(t))
}
})
.attr("transform", "translate(" + x(-1) + ")")
all.on("end", function(d, i) {
d.shift();
this._old = d;
if (d.key === numberSeries-1) { render() }
});
data.shift();
}
在这个函数中,将收到的数据添加到数据数组中,使用标准的选择、进入、合并模式将数据绑定到路径元素。在过渡中,使用自定义插值器更改
d
属性,并使用
x
比例尺将路径元素向左移动一步。
通过以上步骤,我们可以实现流式数据的可视化,从心电图和呼吸信息的实时显示到随机数据驱动的流图,为数据分析和决策提供有力的支持。
通过本文的介绍,你可以看到如何利用WebSocket和D3.js实现流式数据的可视化。无论是心电图和呼吸信息的实时监控,还是随机数据的动态展示,这些技术都能帮助你更好地理解和分析数据。在实际应用中,你可以根据具体需求调整参数和代码,以实现更个性化的可视化效果。希望这些内容对你有所帮助,让你在数据可视化的道路上更进一步。
流式数据可视化:从心电图到随机流图
4. 技术要点总结与对比
为了更清晰地理解上述两种流式数据可视化方法,下面对关键技术点进行总结和对比。
| 可视化类型 | 服务器参数 | 数据处理 | 比例尺设置 | 图形绘制 | 动画处理 |
|---|---|---|---|---|---|
| 心电图和呼吸信息可视化 |
fileName
、
sendInterval
、
toSkip
| 读取外部文件,分割数据并转换为JSON结构 |
x
、
yEcg
、
yResp
、
sEcg
、
sResp
|
d3.line
生成器绘制线
| 每次接收数据更新线和图像透明度 |
| 随机数据驱动的流图 |
n
(数据元素数量)、
sendInterval
| 生成随机数据 |
x
、
y
、
color
|
d3.area
生成器绘制区域
| 过渡动画更新路径,自定义插值器处理数据变化 |
从这个表格可以看出,两种可视化方法在服务器参数、数据处理方式、比例尺设置、图形绘制和动画处理上都有不同的侧重点。心电图可视化更注重对外部文件数据的处理和实时更新,而随机流图则侧重于随机数据的生成和区域图形的动态展示。
5. 实际应用场景与拓展
5.1 医疗监测领域
在医疗监测中,心电图和呼吸信息的可视化可以用于实时监测患者的生命体征。医生可以通过观察心电图和呼吸曲线的变化,及时发现患者的健康问题。例如,在重症监护室中,将患者的心电图和呼吸数据通过WebSocket服务器实时传输到监控屏幕上,医生可以随时查看患者的状态。同时,可以根据患者的历史数据和实时数据进行分析,预测可能出现的健康风险。
为了实现更精准的监测,可以对数据进行进一步的处理,如滤波、降噪等。还可以添加报警功能,当心电图或呼吸数据超出正常范围时,及时通知医生。以下是一个简单的报警功能示例代码:
function process(received) {
// Push a new data point onto the back.
data.push(received);
d3.select(".ecg").attr("d", lineEcg);
d3.select(".resp").attr("d", lineResp);
d3.select(".heartContainer").attr("opacity", sEcg(+received.ecg))
d3.select(".lungContainer").attr("opacity", sResp(+received.resp))
// 报警功能
if (+received.ecg < 15800 || +received.ecg > 26000 || +received.resp < 14800 || +received.resp > 24000) {
alert("患者生命体征异常!");
}
data.shift();
}
5.2 金融市场分析
在金融市场分析中,随机数据驱动的流图可以用于展示不同金融产品的走势。例如,将不同股票的价格数据作为随机数据的来源,通过流图可以直观地看到各股票价格的波动情况和相互关系。投资者可以根据流图的变化,做出更明智的投资决策。
为了使流图更具实用性,可以添加交互功能,如鼠标悬停显示具体数据、点击查看详细信息等。以下是一个简单的鼠标悬停显示数据的示例代码:
var all = existingEls.merge(newEls)
.attr("transform", null)
.transition().duration(interval).ease(d3.easeLinear)
.attrTween("d", function(d, i) {
var oldData = this._old
? this._old
: d3.range(totalDatapoints + 1).map(function() {return {'0': 0, '1': 0}});
var currentData = d;
var interpolator = d3.interpolate(oldData, currentData)
return function(t) {
return area(interpolator(t))
}
})
.attr("transform", "translate(" + x(-1) + ")")
.on("mouseover", function(d) {
// 显示数据
d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0.9)
.html("Series: " + d.key + "<br/>Value: " + d[0][0]);
})
.on("mouseout", function() {
// 隐藏数据
d3.select(".tooltip").remove();
});
6. 未来发展趋势
随着技术的不断发展,流式数据可视化将朝着更加智能化、交互化和多样化的方向发展。
6.1 智能化
未来的流式数据可视化系统将具备更强大的数据分析和预测能力。例如,通过机器学习算法对心电图和呼吸数据进行分析,自动诊断患者的健康问题;对金融市场数据进行预测,提前为投资者提供决策建议。
6.2 交互化
交互性将成为流式数据可视化的重要特征。用户可以通过手势、语音等多种方式与可视化图形进行交互,获取更详细的信息。例如,在医疗监测中,医生可以通过手势放大或缩小心电图曲线,查看更细节的信息。
6.3 多样化
可视化的形式将更加多样化。除了线图和流图,还将出现更多新颖的图形,如三维可视化、动态拓扑图等。这些多样化的可视化形式将更好地满足不同领域的需求。
7. 总结与展望
通过本文对心电图和呼吸信息可视化以及随机数据驱动的流图的详细介绍,我们可以看到流式数据可视化在不同领域的应用潜力。WebSocket服务器和D3.js的结合为实现流式数据的实时可视化提供了强大的工具。
在实际应用中,我们可以根据具体需求选择合适的可视化方法,并对代码进行调整和优化。同时,要关注技术的发展趋势,不断探索新的可视化形式和应用场景。
希望本文能够为你在流式数据可视化方面提供有益的参考,让你在数据可视化的道路上不断创新和进步。
mermaid流程图如下,展示了随机数据驱动的流图的整体流程:
graph LR
A[启动随机数据WebSocket服务器] --> B[生成随机数据]
B --> C[通过WebSocket发送数据]
C --> D[客户端接收数据]
D --> E[处理数据并更新流图]
E --> F[过渡动画展示图形变化]
F --> G{是否继续接收数据}
G -- 是 --> C
G -- 否 --> H[结束]
这个流程图清晰地展示了随机数据驱动的流图从服务器生成数据到客户端更新图形的整个过程,有助于理解整个系统的运行机制。
超级会员免费看
1001

被折叠的 条评论
为什么被折叠?



