Plotly - 根据值隐藏悬停工具提示上的数据?

Pat*_*ime 8 javascript plot plotly plotly.js

当我将鼠标悬停在堆积折线图上时,它会显示不在范围内的所有线条的零。有没有办法隐藏这些值而不是向悬停工具添加噪音?

最小的例子

Plotly.newPlot('test', [{
    line: { shape : 'vh' },
    stackgroup: '1',
    x: [1, 2],
    y: [1, 1],
}, {
    line: { shape : 'vh' },
    stackgroup: '1',
    x: [3, 4],
    y: [2, 2],
}, {
    line: { shape : 'vh' },
    stackgroup: '1',
    x: [3, 4, 5, 6],
    y: [3, 3, 3, 3],
}], {
    hovermode: 'x unified',
    width: '100%',
});
Run Code Online (Sandbox Code Playgroud)

作为jsfiddle和图像:

最小示例 - 显示图例噪声的堆积图

语境

我有一个时间序列图,伸展约 5 年,其中包含每条线跨越 6-12 个月的线。Plotly 用零填充每行,这使得悬停工具非常嘈杂。

绘制悬停图

我想隐藏每个 x 轴日期的“0 小时”条目,方法是确保 Plotly 不会用 0 填充行,或者配置工具提示来动态隐藏值。

Bra*_*ell 4

长话短说

\n

这是最终产品,如下。看看我的逻辑如何利用 CSS 自定义属性来对抗 Plotly 的持久性。不幸的是,在深入研究 Plotly 的文档相当长一段时间后,似乎没有一种简单或本机的方法可以使用 Plotly 的函数来实现此目的,但这不会阻止我们使用我们的知识围绕生态系统的自然法则开展工作。

\n

\r\n
\r\n
const testPlot = document.getElementById(\'test\');\n\nPlotly.newPlot(\'test\', [{\n  line: { shape : \'vh\' },\n  stackgroup: \'1\',\n  x: [1, 2, null, null],\n  y: [1, 1, null, null],\n}, {\n  line: { shape : \'vh\' },\n  stackgroup: \'1\',\n  x: [3, 4, null, null],\n  y: [2, 2, null, null],\n}, {\n  line: { shape : \'vh\' },\n  stackgroup: \'1\',\n  x: [3, 4, 5, 6],\n  y: [3, 3, 3, 3],\n}], {\n  hovermode: \'x unified\',\n  width: \'100%\'\n});\n\nconst addPxToTransform = transform => \'translate(\' + transform.replace(/[^\\d,.]+/g, \'\').split(\',\').map(e => e + \'px\').join(\',\') + \')\';\n\n[\'plotly_hover\', \'plotly_click\', \'plotly_legendclick\', \'plotly_legenddoubleclick\'].forEach(trigger => {\n  testPlot.on(trigger, function(data) {\n    if (document.querySelector(\'.hoverlayer > .legend text.legendtext\')) {\n      const legend = document.querySelector(\'.hoverlayer > .legend\');\n      legend.style.setProperty(\'--plotly-legend-transform\', addPxToTransform(legend.getAttribute(\'transform\')));\n      const legendTexts = Array.from(document.querySelectorAll(\'.hoverlayer > .legend text.legendtext\'));\n      const legendTextGroups = Array.from(document.querySelectorAll(\'.hoverlayer > .legend text.legendtext\')).map(text => text.parentNode);\n      const transformValues = [];\n      legendTexts.filter(label => (transformValues.push(addPxToTransform(label.parentNode.getAttribute(\'transform\'))), label.textContent.includes(\' : 0\'))).forEach(zeroValue => zeroValue.parentNode.classList.add(\'zero-value\'));\n      legendTextGroups.filter(g => !g.classList.contains(\'zero-value\')).forEach((g,i) => g.style.setProperty(\'--plotly-transform\', transformValues[i]));\n      const legendBG = document.querySelector(\'.hoverlayer > .legend > rect.bg\');\n      legendBG.style.setProperty(\'--plotly-legend-bg-height\', Math.floor(legendBG.nextElementSibling.getBBox().height + 10) + \'px\');\n    }\n  });\n});
Run Code Online (Sandbox Code Playgroud)\r\n
.hoverlayer > .legend[style*="--plotly-legend-transform"] {\n  transform: var(--plotly-legend-transform) !important;\n}\n.hoverlayer > .legend > rect.bg[style*="--plotly-legend-bg-height"] {\n  height: var(--plotly-legend-bg-height) !important;\n}\n.hoverlayer > .legend [style*="--plotly-transform"] {\n  transform: var(--plotly-transform) !important;\n}\n.hoverlayer > .legend .zero-value {\n  display: none !important;\n}
Run Code Online (Sandbox Code Playgroud)\r\n
<div id="test"></div>\n<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
Run Code Online (Sandbox Code Playgroud)\r\n
\r\n
\r\n

\n

完整的解释

\n

这真的让我大吃一惊(没有双关语的意思)。我浏览了 Plotly 文档,结果发现虽然它们相当全面,但它们并没有提供像您这样的人在定制方面所希望的所有灵活性。他们确实有一个页面,其中包含有关Plotly JS 过滤器的基本信息基本信息,其中包含一个基本示例,但它实际上不会像您想要的那样从您的图例中删除任何点。

\n

接下来,我在您分配了绘图的 div 上设置了mousemove事件监听器。这开始起作用,但是我的风格和 Plotly 似乎坚持使用的风格之间存在很多闪烁。这让我注意到 Plotly 的脚本非常持久,这在后来派上了用场。接下来,我尝试了与MutationObserver类似的方法,因为它应该更快,但可惜的是,更多的是相同的。

\n

此时,我深入研究 Plotly 文档,找到了关于其自定义内置JS 事件处理程序的页面。我开始使用该plotly_hover活动并且正在取得进展。这个事件的运行频率要低得多,mousemove所以它对浏览器来说不会那么累,而且它只在 Plotly JS 事件上运行,所以它实际上只在你需要它的时候运行,所以这很方便。然而,我仍然遇到一些闪烁的情况。

\n

我意识到我能够注入自己的样式,但 Plotly 很快就会覆盖我尝试覆盖的任何属性。如果我覆盖属性,它会把它改回来。如果我删除了零值图例键,它会刷新图例以重新包含它。我必须发挥创意。当我发现 Plotly 不会覆盖我添加到图例键或其祖先的自定义 CSS 属性时,我终于开始取得重大进展。利用这个以及我已经从 Plotly 构建的 SVG 元素中获得的属性值,我能够相应地操作它们。

\n

虽然还有一些棘手的问题,但我设法全部解决了。我为所有transform图例键(包括具有零值的键)创建了一个包含所有原始属性值的数组,然后在隐藏零值键后,我迭代回那些仍然可见的键,并使用自定义 CSS 属性重置其转换属性,如下所示我提到。如果我尝试添加 CSS 内联,Plotly 很快就会被覆盖,所以我使用了单独的样式表。

\n

存储在 SVG 属性中的值transform没有px,因此我编写了一个快速正则表达式函数来清理它,然后将这些更新的值推送到数组,然后再在键上设置自定义属性。即使迈出了这一步,我还没有走出困境。这使我能够隐藏零值键并重新组织可见键以再次堆叠在顶部(摆脱尴尬的空白),但图例容器本身和图例框仍然有一点闪烁还是原来的高度。

\n

为了解决图例的闪烁位置问题,我对其进行了与按键类似的处理。在我的任何其他逻辑之前,我transform使用 CSS 自定义属性设置图例的属性以保持其持久性,即使 Plotly 试图适应我的更改并覆盖我的样式。然后,为了解决高度问题,在所有其他逻辑的最后,我使用\xe2\x80\x94你猜对了\xe2\x80\x94a自定义CSS属性重新计算了背景(一个单独的矩形元素)的高度。我们也不能在 SVG 元素上使用offsetHeightor clientHeight,所以我使用边界框计算高度elem.getBBox().height

\n

最后的更改\xe2\x80\x94 我还注意到,即使我的样式在大部分情况下都有效,但如果您单击 Plotly 画布,Plotly 似乎会进行快速重置,这是如何抵消这种情况的唯一解释我的风格是,这是一个单独的事件,不会触发我的更改。我重新访问了文档,并添加了一些其他被触发到事件处理程序列表的侦听器。

\n

为了代码干净,我将所有处理程序名称放入数组中,并使用forEach(). 我支持的事件处理程序是plotly_hoverplotly_clickplotly_legendclickplotly_legenddoubleclick

\n

这是成品:

\n

\r\n
\r\n
const testPlot = document.getElementById(\'test\');\n\nPlotly.newPlot(\'test\', [{\n  line: { shape : \'vh\' },\n  stackgroup: \'1\',\n  x: [1, 2, null, null],\n  y: [1, 1, null, null],\n}, {\n  line: { shape : \'vh\' },\n  stackgroup: \'1\',\n  x: [3, 4, null, null],\n  y: [2, 2, null, null],\n}, {\n  line: { shape : \'vh\' },\n  stackgroup: \'1\',\n  x: [3, 4, 5, 6],\n  y: [3, 3, 3, 3],\n}], {\n  hovermode: \'x unified\',\n  width: \'100%\'\n});\n\nconst addPxToTransform = transform => \'translate(\' + transform.replace(/[^\\d,.]+/g, \'\').split(\',\').map(e => e + \'px\').join(\',\') + \')\';\n\n[\'plotly_hover\', \'plotly_click\', \'plotly_legendclick\', \'plotly_legenddoubleclick\'].forEach(trigger => {\n  testPlot.on(trigger, function(data) {\n    if (document.querySelector(\'.hoverlayer > .legend text.legendtext\')) {\n      const legend = document.querySelector(\'.hoverlayer > .legend\');\n      legend.style.setProperty(\'--plotly-legend-transform\', addPxToTransform(legend.getAttribute(\'transform\')));\n      const legendTexts = Array.from(document.querySelectorAll(\'.hoverlayer > .legend text.legendtext\'));\n      const legendTextGroups = Array.from(document.querySelectorAll(\'.hoverlayer > .legend text.legendtext\')).map(text => text.parentNode);\n      const transformValues = [];\n      legendTexts.filter(label => (transformValues.push(addPxToTransform(label.parentNode.getAttribute(\'transform\'))), label.textContent.includes(\' : 0\'))).forEach(zeroValue => zeroValue.parentNode.classList.add(\'zero-value\'));\n      legendTextGroups.filter(g => !g.classList.contains(\'zero-value\')).forEach((g,i) => g.style.setProperty(\'--plotly-transform\', transformValues[i]));\n      const legendBG = document.querySelector(\'.hoverlayer > .legend > rect.bg\');\n      legendBG.style.setProperty(\'--plotly-legend-bg-height\', Math.floor(legendBG.nextElementSibling.getBBox().height + 10) + \'px\');\n    }\n  });\n});
Run Code Online (Sandbox Code Playgroud)\r\n
.hoverlayer > .legend[style*="--plotly-legend-transform"] {\n  transform: var(--plotly-legend-transform) !important;\n}\n.hoverlayer > .legend > rect.bg[style*="--plotly-legend-bg-height"] {\n  height: var(--plotly-legend-bg-height) !important;\n}\n.hoverlayer > .legend [style*="--plotly-transform"] {\n  transform: var(--plotly-transform) !important;\n}\n.hoverlayer > .legend .zero-value {\n  display: none !important;\n}
Run Code Online (Sandbox Code Playgroud)\r\n
<div id="test"></div>\n<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
Run Code Online (Sandbox Code Playgroud)\r\n
\r\n
\r\n

\n

  • 我想尝试一下你的解决方案,因为它真的很酷(如果可以的话我会+5)。我减少并简化了查询选择器的数量,使用了更少的循环,消除了对 `.parentNode` 的需要,并使用 `eventData.points[reverseIdx].y == 0` 而不是寻找 `label.textContent.includes( ': 0')`: https://codepen.io/Alexander9111/pen/bGgPrJE (2认同)