如何使用垂直子图获取分组箱线图
How to get grouped boxplots with vertical subplots
我正在尝试使用 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);
有没有人有什么想法?
免责声明
首先我想强调的是,这是一个 workaraound,因为 Plotly 目前不支持将单个数据源分发到多个轴而不将它们解释为新的trace-instances(尽管像 { yaxis: [ "y", "y2" ] }
那样设置一个目标轴数组会很棒)。
但是,Plotly 在处理轨迹的排序和分组方面具有很强的确定性,我们可以利用这一点。
以下解决方法通过以下方式解决问题:
- 使用两个图表和一个 xaxis/yaxis 而不是两个轴
- 对每条轨迹使用单一数据源(
A
、B
、C
)
- 根据外部决策动态地向每个(或两个)绘图添加轨迹
- 使用以下策略之一插入幽灵对象,从而在相同的 x-axis 位置保留两个地块的痕迹:
- 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
)
然后我们可以将数据分成三个主要部分source-objects:
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-axis 位置
上保留两个地块的痕迹
在我们研究不同的选项之前,让我们看看如果我们删除数据或将数据清空会发生什么。
删除数据
删除数据意味着 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)
结果图看起来很像期望的结果:
您可以使用这个 link 来玩转它并根据您的意愿改进它:https://codepen.io/anon/pen/agzKBV?editors=1010
总结
这个例子仍然被认为是一种解决方法,并且没有在给定的 d 之外进行测试塔。在可重用性和代码效率方面也有改进的空间,并且都按顺序写下来,以使这段代码尽可能易于理解。
另请记住,在两个不同的轴上显示相同的数据可能会被误导为两组不同的数据。
允许任何改进建议,代码可免费使用。
我正在尝试使用 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);
有没有人有什么想法?
免责声明
首先我想强调的是,这是一个 workaraound,因为 Plotly 目前不支持将单个数据源分发到多个轴而不将它们解释为新的trace-instances(尽管像 { yaxis: [ "y", "y2" ] }
那样设置一个目标轴数组会很棒)。
但是,Plotly 在处理轨迹的排序和分组方面具有很强的确定性,我们可以利用这一点。
以下解决方法通过以下方式解决问题:
- 使用两个图表和一个 xaxis/yaxis 而不是两个轴
- 对每条轨迹使用单一数据源(
A
、B
、C
)
- 对每条轨迹使用单一数据源(
- 根据外部决策动态地向每个(或两个)绘图添加轨迹
- 使用以下策略之一插入幽灵对象,从而在相同的 x-axis 位置保留两个地块的痕迹:
- a) 使用不透明度
- b) 使用最小宽度
- c) 使用阈值
- 使用以下策略之一插入幽灵对象,从而在相同的 x-axis 位置保留两个地块的痕迹:
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
)
然后我们可以将数据分成三个主要部分source-objects:
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-axis 位置
上保留两个地块的痕迹在我们研究不同的选项之前,让我们看看如果我们删除数据或将数据清空会发生什么。
删除数据
删除数据意味着 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)
结果图看起来很像期望的结果:
您可以使用这个 link 来玩转它并根据您的意愿改进它:https://codepen.io/anon/pen/agzKBV?editors=1010
总结
这个例子仍然被认为是一种解决方法,并且没有在给定的 d 之外进行测试塔。在可重用性和代码效率方面也有改进的空间,并且都按顺序写下来,以使这段代码尽可能易于理解。
另请记住,在两个不同的轴上显示相同的数据可能会被误导为两组不同的数据。
允许任何改进建议,代码可免费使用。