如何获得具有垂直子图的分组箱图

时间:2019-05-31 15:07:29

标签: javascript plotly.js

我正在尝试使用Plotly.js创建一个类似于该图中的图表的图表: enter image description here

这是一个带有两个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);

有人有什么想法吗?

1 个答案:

答案 0 :(得分:1)

免责声明

首先,我想强调一点,这是一个 workaraound ,因为Plotly当前不支持将单个数据源分布到多轴,而不将其解释为新的跟踪实例(尽管最好设置一个目标轴数组,例如{ yaxis: [ "y", "y2" ] })。

但是,Plotly在处理轨迹的排序和分组方面具有非常确定性,这可以使我们受益。

以下变通办法以下列方式解决该问题:

    1. 使用两个具有一个x轴/ y轴而不是两个轴的图表
    1. 为每个跟踪(ABC)使用单一数据源
    1. 根据外部决策为每个(或两个)图动态添加迹线
    1. 使用以下策略之一插入幻影对象,从而使两个图的迹线都保持在相同的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);

如果未添加其他数据,则生成的空图将如下所示:

enter image description here

2。每个跟踪使用单一数据源(ABC

然后我们可以将数据分为三个主要的源对象:

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 })

这将产生以下结果(但尚不能令人满意):

enter image description here

现在,我们至少已经解决了分组和颜色问题。下一步是寻找制作B幻影对象的可能方法,该幻影对象需要在上部图表中留有间距,但不会显示数据。

4。使用以下策略之一插入幻影对象,从而将两个图的痕迹保持在相同的x轴位置

在研究不同的选项之前,让我们看看如果删除数据或将数据为空会发生什么情况。

删除数据

删除数据将意味着placeholder没有x / y值:

const placeholder = src => {
    const copy = Object.assign({}, src)
    delete copy.x
    delete copy.y
    return copy
}

结果仍然不能满足要求:

enter image description here

清空数据

取消数据的效果很好,将数据添加到图例中(与visible: 'legendonly'基本上具有相同的效果:

const placeholder = src => {
    const copy = Object.assign({}, src)
    copy.x = [null]
    copy.y = [null]
    return copy
}

尽管至少图例分组现在是正确的,但结果仍然不能满足要求:

enter image description here

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的缩放比例:

enter image description here

b)使用最小宽度

使用最小的零值会导致痕迹几乎消失,而仍然保留一小行。

const placeholder = src => {
    const copy = Object.assign({}, src)
    copy.width = 0.000000001
    copy.hoverinfo = "none" // or use "name"
    return copy
}

此示例使分组,定位和图例保持正确,但是缩放比例仍然受到影响,剩余的行可能会被误解(这可能是非常有问题的IMO):

enter image description here

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 })

结果将如下所示:

enter image description here

现在唯一的问题仍然是,在添加新数据之后如何确定范围?幸运的是,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)

生成的图看起来最像期望的结果:

enter image description here

您可以根据需要使用此链接进行修改和改进:https://codepen.io/anon/pen/agzKBV?editors=1010

摘要

此示例仍被认为是解决方法,未经超出给定数据的测试。在可重用性和代码效率方面也有改进的余地,并且以顺序的方式将其全部写下以使该代码尽可能容易理解。

还请记住,在两个不同的轴上显示相同的数据可能会误导为将其解释为两组不同的数据。

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