如何获得带有垂直子图的分组箱线图
Posted
技术标签:
【中文标题】如何获得带有垂直子图的分组箱线图【英文标题】:How to get grouped boxplots with vertical subplots 【发布时间】:2019-10-17 06:01:26 【问题描述】:我正在尝试使用 Plotly.js 创建一个类似于此图像中的图表:
这是一个带有两个 y 轴的分组箱线图(按站点,目前只有一个)。
我设法创建了两个版本,但都不起作用:
-
创建 5 条轨迹(每个框 1 条),以便您可以为每个框定义正确的 y 轴。然后将它们全部放在一起,因为它们是不同的痕迹。
创建 3 条轨迹来表示 A、B 和 C。但是(afaik)我必须为每个轨迹选择一个 y 轴,这意味着我不能在两个 y 轴上有相同的轨迹。
这是方法 1 的代码 (https://codepen.io/wacmemphis/pen/gJQJeO?editors=0010)
var data =[
"x":[
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1"
],
"xaxis":"x",
"yaxis":"y",
"name":"A",
"type":"box",
"boxpoints":false,
"y":[
"3.81",
"3.74",
"3.62",
"3.50",
"3.50",
"3.54"
]
,
"x":[
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1"
],
"xaxis":"x",
"yaxis":"y",
"name":"B",
"type":"box",
"boxpoints":false,
"y":[
"1.54",
"1.54",
"1.60",
"1.41",
"1.65",
"1.47"
]
,
"x":[
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1"
],
"xaxis":"x",
"yaxis":"y",
"name":"C",
"type":"box",
"boxpoints":false,
"y":[
"3.31",
"3.81",
"3.74",
"3.63",
"3.76",
"3.68"
]
,
"x":[
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1"
],
"xaxis":"x2",
"yaxis":"y2",
"name":"A",
"type":"box",
"boxpoints":false,
"y":[
"3.81",
"3.74",
"3.62",
"3.50",
"3.50",
"3.54"
]
,
"x":[
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1",
"Site 1"
],
"xaxis":"x2",
"yaxis":"y2",
"name":"C",
"type":"box",
"boxpoints":false,
"y":[
"3.31",
"3.81",
"3.74",
"3.63",
"3.76",
"3.68"
]
];
var layout =
yaxis:
domain: [0, 0.5],
title: 'axis 1',
,
yaxis2:
domain: [0.5, 1],
title: 'axis2',
,
boxmode: 'group'
;
Plotly.newPlot('myDiv', data, layout);
有人有什么想法吗?
【问题讨论】:
关于那个的两个问题:是只需要使用一个带有两个轴的图表还是两个图表也可以? autorange 是所需的范围方法,还是显示图像中的范围固定为这些值? @Jankapunkt 也可以是两个图表,但单独的痕迹应该在同一个地方,颜色相同。在某些图表中,可能根本不存在跟踪(上面屏幕截图的第一部分为橙色)。 这是可能的,但问题仍然存在,无论您是否知道某些范围,或者您是否完全不知道数据的可能范围(我假设,因为您使用domain
而不是 range
)。也许您可以添加一些关于数据可能变化的小细节以及您选择这两个域(0 - 0.5 和 0.5 - 1)的原因
@Jankapunkt 按照我的理解,0-0.5 和 0.5-1 仅用于确定每个 y 轴的高度比例,但我可能弄错了。在获取数据并将其重新格式化为跟踪之前,我不知道值范围。
这就是为什么我询问可能的阈值,因为您将如何确定 name: "B"
的数据将是 yaxis
的一部分,但不是 yaxis2
的一部分?当然,您只是在示例中手动省略了,但这在输入任意数据时不起作用,其中自动范围将考虑所有要显示的数据,而域将考虑所有数据进行缩放。如果您可以确定两个轴的范围(或任何其他阈值标准以在上轴中省略 B
),我可以为您提供一个有效的示例。
【参考方案1】:
免责声明
首先我想强调的是,这是一个workaraound,因为 Plotly 目前不支持将单个数据源分发到多个轴而不将它们解释为新的跟踪实例(尽管最好只设置一个目标轴数组,如 yaxis: [ "y", "y2" ]
)。
但是,Plotly 在处理跟踪排序和分组的方式上具有很强的确定性,我们可以利用这一点。
以下解决方法通过以下方式解决问题:
-
使用带有一个 xaxis/yaxis 而不是两个轴的两个图表
-
对每个跟踪使用单一数据源(
A
、B
、C
)
-
根据外部决策为每个(或两个)图动态添加跟踪
-
使用以下策略之一插入幽灵对象,从而将两个绘图的轨迹保持在相同的 x 轴位置:
a) 使用不透明度
b) 使用最小宽度
c) 使用阈值
1。使用两个图表而不是两个轴
假设我们可以使用两个具有相同布局的图表:
<head>
<!-- Plotly.js -->
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
</head>
<body>
<!-- render the upper axis 2 chart -->
<div id="myDiv_upper"></div>
<!-- render the lower axis 1 chart -->
<div id="myDiv_lower"></div>
<script>
/* javascript CODE GOES HERE */
</script>
</body>
使用随附的 js 代码创建两个具有给定布局的初始空图表:
const myDiv = document.getElementById("myDiv_lower");
const myDiv2 = document.getElementById("myDiv_upper");
const layout =
yaxis:
domain: [0, 0.5],
title: "axis 1",
constrain: "range"
,
margin:
t: 0,
b: 0,
pad: 0
,
showlegend: false,
boxmode: "group"
;
const layout2 =
yaxis:
domain: [ 0.5, 1 ],
title: "axis 2",
,
xaxis:
domain: [ 0, 1 ]
,
margin:
t: 0,
b: 0,
pad: 0
,
boxmode: "group"
;
Plotly.newPlot(myDiv, [], layout);
Plotly.newPlot(myDiv2, [], layout2);
如果没有添加更多数据,生成的空图将如下所示:
2。为每个跟踪使用单一数据源(A
、B
、C
)
然后我们可以将数据分成三个主要的源对象:
const A =
x: ["Site 1", "Site 1", "Site 1", "Site 1", "Site 1", "Site 1"],
xaxis: "x",
yaxis: "y",
name: "A",
legendgroup: "A",
type: "box",
boxpoints: false,
y: ["3.81", "3.74", "3.62", "3.50", "3.50", "3.54"]
;
const B =
x: ["Site 1", "Site 1", "Site 1", "Site 1", "Site 1", "Site 1"],
xaxis: "x",
yaxis: "y",
name: "B",
legendgroup: "B",
type: "box",
boxpoints: false,
y: ["1.54", "1.54", "1.60", "1.41", "1.65", "1.47"]
;
const C =
x: ["Site 1", "Site 1", "Site 1", "Site 1", "Site 1", "Site 1"],
xaxis: "x",
yaxis: "y",
name: "C",
legendgroup: "C",
type: "box",
boxpoints: false,
y: ["3.31", "3.81", "3.74", "3.63", "3.76", "3.68"]
3。根据外部决策为每个(或两个)图动态添加跟踪
首先,我们创建一个帮助器add
,它根据新传入的数据更新图表,另一个创建我们的幽灵对象帮助器,命名为placeholder
:
const placeholder = src =>
const copy = Object.assign(, src)
// use one of the strategies here to make this a ghost object
return copy
const add = ( src, y1, y2 ) =>
let src2
if (y1 && y2)
Plotly.addTraces(myDiv, [src])
Plotly.addTraces(myDiv2, [src])
else if (y1 && !y2)
src2 = placeholder(src)
Plotly.addTraces(myDiv, [src])
Plotly.addTraces(myDiv2, [src2])
else if (!y1 && y2)
src2 = placeholder(src)
Plotly.addTraces(myDiv, [src2])
Plotly.addTraces(myDiv2, [src])
else
throw new Error('require either y1 or y2 to be true to add data')
根据给定的图像,将数据添加到轴的决定将导致以下调用:
add( src: A, y1: true, y2: true )
add( src: B, y1: true, y2: false )
add( src: C, y1: true, y2: true )
这将产生以下(但不能满足)结果:
现在我们至少解决了分组和颜色问题。下一步是寻找使B
成为幽灵对象的可能方法,这需要在上图中留出间距但不会显示数据。
4。使用以下策略之一插入重影对象,从而将两个绘图的轨迹保持在相同的 x 轴位置
在我们研究不同的选项之前,让我们看看如果我们删除数据或将数据设为空会发生什么。
删除数据
删除数据意味着placeholder
没有 x/y 值:
const placeholder = src =>
const copy = Object.assign(, src)
delete copy.x
delete copy.y
return copy
结果仍然不能满足要求:
清空数据
清空数据有很好的效果,将数据添加到图例中(与visible: 'legendonly'
的效果基本相同:
const placeholder = src =>
const copy = Object.assign(, src)
copy.x = [null]
copy.y = [null]
return copy
结果仍然不能满足要求,尽管至少图例分组现在是正确的:
a) 使用不透明度
创建幽灵对象的一个选项是将其不透明度设置为零:
const placeholder = src =>
const copy = Object.assign(, src)
copy.opacity = 0
copy.hoverinfo = "none" // use "name" to show "B"
return copy
结果具有优势,它使对象处于正确的位置。一个很大的缺点是,B 的图例不透明度绑定到对象的不透明度,这仅显示标签 B
而不是彩色框。
另一个缺点是B
的数据仍然影响yaxis
的缩放:
b) 使用最小宽度
使用最小量大于零会导致迹线几乎消失,而留下一条小线。
const placeholder = src =>
const copy = Object.assign(, src)
copy.width = 0.000000001
copy.hoverinfo = "none" // or use "name"
return copy
此示例保持分组、定位和图例正确,但缩放仍然受到影响,并且剩余的行可能会被误解(这可能会给 IMO 带来很大问题):
c) 使用阈值
现在这是满足所有要求的唯一解决方案,但需要注意的是:它需要在 yaxis 上设置 range
:
const layout2 =
yaxis:
domain: [ 0.5, 1 ],
title: "axis 2",
range: [3.4, 4] // this is hardcoded for now
,
xaxis:
domain: [ 0, 1 ]
,
margin:
t: 0,
b: 0,
pad: 0
,
boxmode: "group"
// ...
// with ranges we can safely add
// data to both charts, because they
// get ghosted, based on their fit
// within / outside the range
const add = ( src ) =>
Plotly.addTraces(myDiv, [src])
Plotly.addTraces(myDiv2, [src])
add( src: A )
add( src: B )
add( src: C )
结果将如下所示:
现在唯一的问题是,添加新数据后如何确定范围?幸运的是 Plotly 提供了一个更新布局的函数,名为Plotly.relayout
。
对于这个例子,我们可以选择一个简单的锚点,比如平均值。当然,任何其他确定范围的方法都是可能的。
const add = ( src ) =>
Plotly.addTraces(myDiv, [src])
Plotly.addTraces(myDiv2, [src])
return src.y
// add the data and generate a sum of all values
const avalues = add( src: A )
const bvalues = add( src: B )
const cvalues = add( src: C )
const allValues = [].concat(avalues, bvalues, cvalues)
// some reusable helpers to determine our range
const highest = arr => Math.max.apply( Math, arr )
const mean = arr => arr.reduce((a, b) => Number(a) + Number(b), 0) / arr.length
const upperRange = highest(allValues) // 3.81
const meanRange = mean(allValues) // 2.9361111111111113
// our new values to update the upper layour
const updatedLayout =
yaxis:
range: [meanRange, upperRange]
Plotly.relayout(myDiv2, updatedLayout)
生成的图表看起来很像预期的结果:
您可以使用此链接随意玩转和改进它:https://codepen.io/anon/pen/agzKBV?editors=1010
总结
此示例仍被视为一种解决方法,并且未在给定数据之外进行测试。在可重用性和代码效率方面也有改进的空间,所有这些都是按顺序写下来的,以使这段代码尽可能地易于理解。
还请记住,在两个不同的轴上显示相同的数据可能会被误解为两组不同的数据。
欢迎提出任何改进建议,代码免费使用。
【讨论】:
感谢您非常详细的回答!它向我展示了一些不同的选择,我现在可以去调查。再次感谢!以上是关于如何获得带有垂直子图的分组箱线图的主要内容,如果未能解决你的问题,请参考以下文章