如何使用垂直子图获取分组箱线图

How to get grouped boxplots with vertical subplots

我正在尝试使用 Plotly.js 创建一个类似于此图片中的图表:

这是一个带有两个 y 轴的分组箱线图(按站点,目前只有一个)。

我已经成功创建了两个版本,但都不起作用:

  1. 创建 5 条轨迹(每个框 1 个),以便您可以为每个框定义正确的 y 轴。然后将它们放在一起,因为它们是不同的痕迹。
  2. 创建 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 在处理轨迹的排序和分组方面具有很强的确定性,我们可以利用这一点。

以下解决方法通过以下方式解决问题:

    1. 使用两个图表和一个 xaxis/yaxis 而不是两个轴
    1. 对每条轨迹使用单一数据源(ABC
    1. 根据外部决策动态地向每个(或两个)绘图添加轨迹
    1. 使用以下策略之一插入幽灵对象,从而在相同的 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。为每个轨迹使用单一数据源 (ABC)

然后我们可以将数据分成三个主要部分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 之外进行测试塔。在可重用性和代码效率方面也有改进的空间,并且都按顺序写下来,以使这段代码尽可能易于理解。

另请记住,在两个不同的轴上显示相同的数据可能会被误导为两组不同的数据。

允许任何改进建议,代码可免费使用。