Echarts:绘制信号的方差
Echarts: Plot the variance of signals
我想在图表中绘制多个信号的方差(或者基本上填满上下信号之间的 space)。
是否可以创建此类图表?
我看到了置信带示例 (https://echarts.apache.org/examples/en/editor.html?c=confidence-band),但这似乎只对图表中的一个信号有效。
另一个解决方案是在信号周围使用 markArea 绘制数千个小矩形,但这会降低图表的性能(例如,滚动 x 轴时)并且看起来不是很平滑。
据我所知,Echarts社区的常见做法是用series
(read docs)绘制常用图表类型(柱状图,折线图,...),并用custom series
编写可视化逻辑为独一无二。 Echarts 还有一些 API 方法(未记录),如 registerVisual
、registerLayout
可用于重新定义布局、计算等。
对于所描述的任务,您需要使用 custom
系列来计算波段坐标。这不是很简单,因为(在我看来)具有置信区间的强制性要求很少见。
关于性能。 Echarts 默认使用 Canvas 渲染可视化部分。通常 Canvas 在 HTML 中没有太多用于显示图表的部分,它只是浏览器渲染的图像数据,几乎不需要显示多少数据点。换句话说,我们看到的是 PNG,而不是很多 div
、svg
、g
和其他像 SVG 中那样具有几何基元的层,但是繁重的计算复杂的业务逻辑可能会影响响应能力UI 与其他图表一样。
下面的示例我将如何实现此功能。我不确定这是正确的方法,但它可以工作并且可以调整。
var dates = ['2020-01-03','2020-01-31','2020-02-17','2020-02-18','2020-03-13','2020-04-10','2020-05-01','2020-05-19','2020-05-22','2020-05-25'];
var sensor1 = [0.6482086334797242, 0.9121368038482911, 0.3205730196548609, 0.8712238348969002, 0.4487714576177558, 0.9895025457815625, 0.0415490306934774, 0.1592908349676395, 0.5356690594518069, 0.9949108727912939];
var sensor2 = [0.8278430459565170, 0.5700757488718124, 0.9803575576802187, 0.0770264671179814,0.2843735619252158,0.8140209568127250,0.6055633547296827,0.9554255125528607,0.1703504100638565,0.5653245914197297];
// Calculate fake bands coordinates
function calcContourCoords(seriesData, ctx){
var addNoise = idx => Math.round(Math.random() * 8 * idx);
var pixelCoords = seriesData.map((dataPoint, idx) => {
return [
ctx.convertToPixel({ xAxisIndex: 0 }, idx) + addNoise(idx),
ctx.convertToPixel({ yAxisIndex: 0 }, dataPoint) + addNoise(idx)
]
});
var polyfilltype = ClipperLib.PolyFillType.pftEvenOdd;
var linePath = new ClipperLib.Path();
var delta = 15;
var scale = 1;
for (var i = 0; i < pixelCoords.length; i++){
var point = new ClipperLib.IntPoint(...pixelCoords[i]);
linePath.push(point);
}
var co = new ClipperLib.ClipperOffset(1.0, 0.25);
co.AddPath(linePath, ClipperLib.JoinType.jtRound, ClipperLib.EndType.etOpenSquare);
co.Execute(linePath, delta * scale);
return co.m_destPoly.map(c => [c.X, c.Y])
}
// Render visual by calculated coords
function renderItem(params, api){
// Prevent multiple call
if (params.context.rendered) return;
params.context.rendered = true;
// Get stored in series data for band
var series = myChart.getModel().getSeriesByName(params.seriesName)[0];
var seriesData = series.get('data');
// Calculate band coordinates for series
var bandCoords = calcContourCoords(seriesData, myChart);
// Draw band
return {
type: 'polygon',
shape: {
points: echarts.graphic.clipPointsByRect(bandCoords, {
x: params.coordSys.x,
y: params.coordSys.y,
width: params.coordSys.width,
height: params.coordSys.height
})
},
style: api.style({
fill: series.option.itemStyle.color
})
};
}
// =============
var option = {
tooltip: {},
legend: {
data:['Label']
},
xAxis: [
{ name: 'x0', data: dates, boundaryGap: true },
{ name: 'x1', data: dates, boundaryGap: true, show: false },
],
yAxis: [
{ name: 'y0' },
{ name: 'y1', show: false },
],
series: [
// First line
{
name: 'Sensor1',
type: 'line',
data: sensor1,
itemStyle: { color: 'rgba(69, 170, 242, 1)' },
yAxisIndex: 0,
xAxisIndex: 0,
},
{
name: 'BandSensor1',
type: 'custom',
data: sensor1,
itemStyle: { color: 'rgba(69, 170, 242, 0.2)' },
renderItem: renderItem,
yAxisIndex: 0,
xAxisIndex: 0,
},
// Second line
{
name: 'Sensor2',
type: 'line',
data: sensor2,
itemStyle: { color: 'rgba(253, 151, 68, 1)' },
yAxisIndex: 1,
xAxisIndex: 1,
},
{
name: 'BandSensor2',
type: 'custom',
data: sensor2,
itemStyle: { color: 'rgba(253, 151, 68, 0.2)' },
renderItem: renderItem,
yAxisIndex: 1,
xAxisIndex: 1,
},
]
};
var myChart = echarts.init(document.getElementById('main'));
myChart.setOption(option);
<script src="https://cdn.jsdelivr.net/npm/clipper-lib@6.4.2/clipper.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/4.7.0/echarts.min.js"></script>
<div id="main" style="width: 800px;height:600px;"></div>
@Sergey Fedorov 的改进版本-此解决方案考虑了最小值、最大值或。带的动态边界厚度
var dates = ['2020-01-03', '2020-01-31', '2020-02-17', '2020-02-18', '2020-03-13', '2020-04-10', '2020-05-01', '2020-05-19', '2020-05-22', '2020-05-25', '2020-05-27'];
const data_raw1 = [
{ min: -5, mean: 0, max: 0 },
{ min: 1, mean: 2, max: 5 },
{ min: 2, mean: 4, max: 6 },
{ min: 4, mean: 5, max: 8 },
{ min: 7, mean: 11, max: 14 },
{ min: 11, mean: 15, max: 17 },
{ min: 6, mean: 8, max: 8.5 },
{ min: -1, mean: 5, max: 6 },
{ min: 4, mean: 9, max: 12 },
{ min: 14, mean: 18, max: 22 },
{ min: 18, mean: 20, max: 21 },
];
const data_raw2 = [
{ min: 10, mean: 15, max: 20 },
{ min: 12, mean: 25, max: 30 },
{ min: 22, mean: 26, max: 32 },
{ min: 30, mean: 31, max: 45 },
{ min: 47, mean: 49, max: 50 },
{ min: 30, mean: 32, max: 41 },
{ min: 34, mean: 36, max: 38 },
{ min: 40, mean: 42, max: 45 },
{ min: 47, mean: 49, max: 56 },
{ min: 60, mean: 68, max: 70 },
{ min: 75, mean: 80, max: 85 },
];
const data_raw3 = data_raw2.map(d => ({ min: d.min * 1.2 + 10, mean: d.mean * 1.4 + 11, max: d.max * 1.5 + 12 }))
function calcContourCoords(seriesData, ctx) {
console.log("seriesData=", seriesData);
const pixelCoords = []
for (let i = 0; i < seriesData.length; i++) {
console.log(i, seriesData[i]);
pixelCoords.push([
ctx.convertToPixel({ xAxisIndex: 0 }, i),
ctx.convertToPixel({ yAxisIndex: 0 }, seriesData[i].max)
]);
}
console.log("\n")
for (let i = seriesData.length - 1; i >= 0; i--) {
console.log(i, seriesData[i]);
pixelCoords.push([
ctx.convertToPixel({ xAxisIndex: 0 }, i),
ctx.convertToPixel({ yAxisIndex: 0 }, seriesData[i].min)
]);
if (i == 0) {
pixelCoords.push([
ctx.convertToPixel({ xAxisIndex: 0 }, i),
ctx.convertToPixel({ yAxisIndex: 0 }, seriesData[i].max)
]);
}
}
var linePath = new ClipperLib.Path();
var delta = 10;
var scale = 1;
for (var i = 0; i < pixelCoords.length; i++) {
var point = new ClipperLib.IntPoint(...pixelCoords[i]);
linePath.push(point);
}
var co = new ClipperLib.ClipperOffset(1.0, 0.25);
co.AddPath(linePath, ClipperLib.JoinType.jtRound, ClipperLib.EndType.etClosedPolygon);
co.Execute(linePath, delta * scale);
return co.m_destPoly.map(c => [c.X, c.Y])
}
// Render visual by calculated coords
function renderItem(params, api) {
// Prevent multiple call
if (params.context.rendered) return;
params.context.rendered = true;
// Get stored in series data for band
var series = myChart.getModel().getSeriesByName(params.seriesName)[0];
var seriesData = series.get('data');
// Calculate band coordinates for series
var bandCoords = calcContourCoords(seriesData, myChart);
// Draw band
return {
type: 'polygon',
shape: {
points: echarts.graphic.clipPointsByRect(bandCoords, {
x: params.coordSys.x,
y: params.coordSys.y,
width: params.coordSys.width,
height: params.coordSys.height
})
},
style: api.style({
fill: series.option.itemStyle.color
})
};
}
// =============
var option = {
tooltip: {},
legend: {
data: ['Label']
},
xAxis: [
{ name: 'x0', data: dates, boundaryGap: true },
{ name: 'x1', data: dates, boundaryGap: true, show: false },
],
yAxis: [
{ name: 'y0' },
{ name: 'y1', show: false },
],
series: [
// First line
{
name: 'Sensor1',
type: 'line',
data: data_raw1.map(d => d.mean),
itemStyle: { color: 'rgba(69, 170, 242, 1)' },
yAxisIndex: 0,
xAxisIndex: 0,
itemStyle: { color: 'red' },
},
{
name: 'BandSensor1',
type: 'custom',
data: data_raw1,
itemStyle: { color: 'rgba(69, 170, 242, 0.2)' },
renderItem: renderItem,
yAxisIndex: 0,
xAxisIndex: 0,
itemStyle: { color: 'red', opacity: 0.1 },
},
{
name: 'Sensor2',
type: 'line',
data: data_raw2.map(d => d.mean),
itemStyle: { color: 'rgba(69, 170, 242, 1)' },
yAxisIndex: 0,
xAxisIndex: 0,
itemStyle: { color: 'blue' },
},
{
name: 'BandSensor2',
type: 'custom',
data: data_raw2,
itemStyle: { color: 'rgba(69, 170, 242, 0.2)' },
renderItem: renderItem,
yAxisIndex: 0,
xAxisIndex: 0,
itemStyle: { color: 'blue', opacity: 0.1 },
},
{
name: 'Sensor3',
type: 'line',
data: data_raw3.map(d => d.mean),
itemStyle: { color: 'rgba(69, 170, 242, 1)' },
yAxisIndex: 0,
xAxisIndex: 0,
itemStyle: { color: 'green' },
},
{
name: 'BandSensor3',
type: 'custom',
data: data_raw3,
itemStyle: { color: 'rgba(69, 170, 242, 0.2)' },
renderItem: renderItem,
yAxisIndex: 0,
xAxisIndex: 0,
itemStyle: { color: 'green', opacity: 0.1 },
},
]
};
var myChart = echarts.init(document.getElementById('chart'));
myChart.setOption(option);
我想在图表中绘制多个信号的方差(或者基本上填满上下信号之间的 space)。 是否可以创建此类图表?
我看到了置信带示例 (https://echarts.apache.org/examples/en/editor.html?c=confidence-band),但这似乎只对图表中的一个信号有效。
另一个解决方案是在信号周围使用 markArea 绘制数千个小矩形,但这会降低图表的性能(例如,滚动 x 轴时)并且看起来不是很平滑。
据我所知,Echarts社区的常见做法是用series
(read docs)绘制常用图表类型(柱状图,折线图,...),并用custom series
编写可视化逻辑为独一无二。 Echarts 还有一些 API 方法(未记录),如 registerVisual
、registerLayout
可用于重新定义布局、计算等。
对于所描述的任务,您需要使用 custom
系列来计算波段坐标。这不是很简单,因为(在我看来)具有置信区间的强制性要求很少见。
关于性能。 Echarts 默认使用 Canvas 渲染可视化部分。通常 Canvas 在 HTML 中没有太多用于显示图表的部分,它只是浏览器渲染的图像数据,几乎不需要显示多少数据点。换句话说,我们看到的是 PNG,而不是很多 div
、svg
、g
和其他像 SVG 中那样具有几何基元的层,但是繁重的计算复杂的业务逻辑可能会影响响应能力UI 与其他图表一样。
下面的示例我将如何实现此功能。我不确定这是正确的方法,但它可以工作并且可以调整。
var dates = ['2020-01-03','2020-01-31','2020-02-17','2020-02-18','2020-03-13','2020-04-10','2020-05-01','2020-05-19','2020-05-22','2020-05-25'];
var sensor1 = [0.6482086334797242, 0.9121368038482911, 0.3205730196548609, 0.8712238348969002, 0.4487714576177558, 0.9895025457815625, 0.0415490306934774, 0.1592908349676395, 0.5356690594518069, 0.9949108727912939];
var sensor2 = [0.8278430459565170, 0.5700757488718124, 0.9803575576802187, 0.0770264671179814,0.2843735619252158,0.8140209568127250,0.6055633547296827,0.9554255125528607,0.1703504100638565,0.5653245914197297];
// Calculate fake bands coordinates
function calcContourCoords(seriesData, ctx){
var addNoise = idx => Math.round(Math.random() * 8 * idx);
var pixelCoords = seriesData.map((dataPoint, idx) => {
return [
ctx.convertToPixel({ xAxisIndex: 0 }, idx) + addNoise(idx),
ctx.convertToPixel({ yAxisIndex: 0 }, dataPoint) + addNoise(idx)
]
});
var polyfilltype = ClipperLib.PolyFillType.pftEvenOdd;
var linePath = new ClipperLib.Path();
var delta = 15;
var scale = 1;
for (var i = 0; i < pixelCoords.length; i++){
var point = new ClipperLib.IntPoint(...pixelCoords[i]);
linePath.push(point);
}
var co = new ClipperLib.ClipperOffset(1.0, 0.25);
co.AddPath(linePath, ClipperLib.JoinType.jtRound, ClipperLib.EndType.etOpenSquare);
co.Execute(linePath, delta * scale);
return co.m_destPoly.map(c => [c.X, c.Y])
}
// Render visual by calculated coords
function renderItem(params, api){
// Prevent multiple call
if (params.context.rendered) return;
params.context.rendered = true;
// Get stored in series data for band
var series = myChart.getModel().getSeriesByName(params.seriesName)[0];
var seriesData = series.get('data');
// Calculate band coordinates for series
var bandCoords = calcContourCoords(seriesData, myChart);
// Draw band
return {
type: 'polygon',
shape: {
points: echarts.graphic.clipPointsByRect(bandCoords, {
x: params.coordSys.x,
y: params.coordSys.y,
width: params.coordSys.width,
height: params.coordSys.height
})
},
style: api.style({
fill: series.option.itemStyle.color
})
};
}
// =============
var option = {
tooltip: {},
legend: {
data:['Label']
},
xAxis: [
{ name: 'x0', data: dates, boundaryGap: true },
{ name: 'x1', data: dates, boundaryGap: true, show: false },
],
yAxis: [
{ name: 'y0' },
{ name: 'y1', show: false },
],
series: [
// First line
{
name: 'Sensor1',
type: 'line',
data: sensor1,
itemStyle: { color: 'rgba(69, 170, 242, 1)' },
yAxisIndex: 0,
xAxisIndex: 0,
},
{
name: 'BandSensor1',
type: 'custom',
data: sensor1,
itemStyle: { color: 'rgba(69, 170, 242, 0.2)' },
renderItem: renderItem,
yAxisIndex: 0,
xAxisIndex: 0,
},
// Second line
{
name: 'Sensor2',
type: 'line',
data: sensor2,
itemStyle: { color: 'rgba(253, 151, 68, 1)' },
yAxisIndex: 1,
xAxisIndex: 1,
},
{
name: 'BandSensor2',
type: 'custom',
data: sensor2,
itemStyle: { color: 'rgba(253, 151, 68, 0.2)' },
renderItem: renderItem,
yAxisIndex: 1,
xAxisIndex: 1,
},
]
};
var myChart = echarts.init(document.getElementById('main'));
myChart.setOption(option);
<script src="https://cdn.jsdelivr.net/npm/clipper-lib@6.4.2/clipper.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/4.7.0/echarts.min.js"></script>
<div id="main" style="width: 800px;height:600px;"></div>
@Sergey Fedorov 的改进版本-此解决方案考虑了最小值、最大值或。带的动态边界厚度
var dates = ['2020-01-03', '2020-01-31', '2020-02-17', '2020-02-18', '2020-03-13', '2020-04-10', '2020-05-01', '2020-05-19', '2020-05-22', '2020-05-25', '2020-05-27'];
const data_raw1 = [
{ min: -5, mean: 0, max: 0 },
{ min: 1, mean: 2, max: 5 },
{ min: 2, mean: 4, max: 6 },
{ min: 4, mean: 5, max: 8 },
{ min: 7, mean: 11, max: 14 },
{ min: 11, mean: 15, max: 17 },
{ min: 6, mean: 8, max: 8.5 },
{ min: -1, mean: 5, max: 6 },
{ min: 4, mean: 9, max: 12 },
{ min: 14, mean: 18, max: 22 },
{ min: 18, mean: 20, max: 21 },
];
const data_raw2 = [
{ min: 10, mean: 15, max: 20 },
{ min: 12, mean: 25, max: 30 },
{ min: 22, mean: 26, max: 32 },
{ min: 30, mean: 31, max: 45 },
{ min: 47, mean: 49, max: 50 },
{ min: 30, mean: 32, max: 41 },
{ min: 34, mean: 36, max: 38 },
{ min: 40, mean: 42, max: 45 },
{ min: 47, mean: 49, max: 56 },
{ min: 60, mean: 68, max: 70 },
{ min: 75, mean: 80, max: 85 },
];
const data_raw3 = data_raw2.map(d => ({ min: d.min * 1.2 + 10, mean: d.mean * 1.4 + 11, max: d.max * 1.5 + 12 }))
function calcContourCoords(seriesData, ctx) {
console.log("seriesData=", seriesData);
const pixelCoords = []
for (let i = 0; i < seriesData.length; i++) {
console.log(i, seriesData[i]);
pixelCoords.push([
ctx.convertToPixel({ xAxisIndex: 0 }, i),
ctx.convertToPixel({ yAxisIndex: 0 }, seriesData[i].max)
]);
}
console.log("\n")
for (let i = seriesData.length - 1; i >= 0; i--) {
console.log(i, seriesData[i]);
pixelCoords.push([
ctx.convertToPixel({ xAxisIndex: 0 }, i),
ctx.convertToPixel({ yAxisIndex: 0 }, seriesData[i].min)
]);
if (i == 0) {
pixelCoords.push([
ctx.convertToPixel({ xAxisIndex: 0 }, i),
ctx.convertToPixel({ yAxisIndex: 0 }, seriesData[i].max)
]);
}
}
var linePath = new ClipperLib.Path();
var delta = 10;
var scale = 1;
for (var i = 0; i < pixelCoords.length; i++) {
var point = new ClipperLib.IntPoint(...pixelCoords[i]);
linePath.push(point);
}
var co = new ClipperLib.ClipperOffset(1.0, 0.25);
co.AddPath(linePath, ClipperLib.JoinType.jtRound, ClipperLib.EndType.etClosedPolygon);
co.Execute(linePath, delta * scale);
return co.m_destPoly.map(c => [c.X, c.Y])
}
// Render visual by calculated coords
function renderItem(params, api) {
// Prevent multiple call
if (params.context.rendered) return;
params.context.rendered = true;
// Get stored in series data for band
var series = myChart.getModel().getSeriesByName(params.seriesName)[0];
var seriesData = series.get('data');
// Calculate band coordinates for series
var bandCoords = calcContourCoords(seriesData, myChart);
// Draw band
return {
type: 'polygon',
shape: {
points: echarts.graphic.clipPointsByRect(bandCoords, {
x: params.coordSys.x,
y: params.coordSys.y,
width: params.coordSys.width,
height: params.coordSys.height
})
},
style: api.style({
fill: series.option.itemStyle.color
})
};
}
// =============
var option = {
tooltip: {},
legend: {
data: ['Label']
},
xAxis: [
{ name: 'x0', data: dates, boundaryGap: true },
{ name: 'x1', data: dates, boundaryGap: true, show: false },
],
yAxis: [
{ name: 'y0' },
{ name: 'y1', show: false },
],
series: [
// First line
{
name: 'Sensor1',
type: 'line',
data: data_raw1.map(d => d.mean),
itemStyle: { color: 'rgba(69, 170, 242, 1)' },
yAxisIndex: 0,
xAxisIndex: 0,
itemStyle: { color: 'red' },
},
{
name: 'BandSensor1',
type: 'custom',
data: data_raw1,
itemStyle: { color: 'rgba(69, 170, 242, 0.2)' },
renderItem: renderItem,
yAxisIndex: 0,
xAxisIndex: 0,
itemStyle: { color: 'red', opacity: 0.1 },
},
{
name: 'Sensor2',
type: 'line',
data: data_raw2.map(d => d.mean),
itemStyle: { color: 'rgba(69, 170, 242, 1)' },
yAxisIndex: 0,
xAxisIndex: 0,
itemStyle: { color: 'blue' },
},
{
name: 'BandSensor2',
type: 'custom',
data: data_raw2,
itemStyle: { color: 'rgba(69, 170, 242, 0.2)' },
renderItem: renderItem,
yAxisIndex: 0,
xAxisIndex: 0,
itemStyle: { color: 'blue', opacity: 0.1 },
},
{
name: 'Sensor3',
type: 'line',
data: data_raw3.map(d => d.mean),
itemStyle: { color: 'rgba(69, 170, 242, 1)' },
yAxisIndex: 0,
xAxisIndex: 0,
itemStyle: { color: 'green' },
},
{
name: 'BandSensor3',
type: 'custom',
data: data_raw3,
itemStyle: { color: 'rgba(69, 170, 242, 0.2)' },
renderItem: renderItem,
yAxisIndex: 0,
xAxisIndex: 0,
itemStyle: { color: 'green', opacity: 0.1 },
},
]
};
var myChart = echarts.init(document.getElementById('chart'));
myChart.setOption(option);