【问题标题】:Plotly - Hide data on hover tooltip depending on value?Plotly - 根据值隐藏悬停工具提示上的数据?
【发布时间】:2021-07-08 16:10:01
【问题描述】:

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

小例子

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%',
});

作为jsfiddle 和图片:

上下文

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

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

【问题讨论】:

  • 代码和数据sample,请。
  • @vestland 已更新,谢谢!
  • @PattimusPrime 如果有机会请看看我的解决方案,它使用 Plotly 内置的自定义 JS 事件处理程序和 CSS 自定义属性来规避他们的插件的持久性。 *** 我的解决方案仅针对您问题的第一个示例。如果您发布第二个示例的源代码,我也很乐意解决这个问题。它应该只需要一个小的调整。干杯!

标签: javascript plot plotly plotly.js


【解决方案1】:

TL;DR

这是最终产品,如下所示。看看我的逻辑如何使用 CSS 自定义属性来对抗 Plotly 的持久性。不幸的是,在深入研究 Plotly 的文档很长一段时间后,似乎没有一种简单或原生的方法可以使用 Plotly 的函数来实现这一点,但这不会阻止我们利用我们的知识来解决他们生态系统的自然法则.

const testPlot = document.getElementById('test');

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

const addPxToTransform = transform => 'translate(' + transform.replace(/[^\d,.]+/g, '').split(',').map(e => e + 'px').join(',') + ')';

['plotly_hover', 'plotly_click', 'plotly_legendclick', 'plotly_legenddoubleclick'].forEach(trigger => {
  testPlot.on(trigger, function(data) {
    if (document.querySelector('.hoverlayer > .legend text.legendtext')) {
      const legend = document.querySelector('.hoverlayer > .legend');
      legend.style.setProperty('--plotly-legend-transform', addPxToTransform(legend.getAttribute('transform')));
      const legendTexts = Array.from(document.querySelectorAll('.hoverlayer > .legend text.legendtext'));
      const legendTextGroups = Array.from(document.querySelectorAll('.hoverlayer > .legend text.legendtext')).map(text => text.parentNode);
      const transformValues = [];
      legendTexts.filter(label => (transformValues.push(addPxToTransform(label.parentNode.getAttribute('transform'))), label.textContent.includes(' : 0'))).forEach(zeroValue => zeroValue.parentNode.classList.add('zero-value'));
      legendTextGroups.filter(g => !g.classList.contains('zero-value')).forEach((g,i) => g.style.setProperty('--plotly-transform', transformValues[i]));
      const legendBG = document.querySelector('.hoverlayer > .legend > rect.bg');
      legendBG.style.setProperty('--plotly-legend-bg-height', Math.floor(legendBG.nextElementSibling.getBBox().height + 10) + 'px');
    }
  });
});
.hoverlayer > .legend[style*="--plotly-legend-transform"] {
  transform: var(--plotly-legend-transform) !important;
}
.hoverlayer > .legend > rect.bg[style*="--plotly-legend-bg-height"] {
  height: var(--plotly-legend-bg-height) !important;
}
.hoverlayer > .legend [style*="--plotly-transform"] {
  transform: var(--plotly-transform) !important;
}
.hoverlayer > .legend .zero-value {
  display: none !important;
}
<div id="test"></div>
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>

完整解释

这个真的让我大吃一惊(不是双关语)。我搜索了 Plotly 文档,却发现虽然它们非常全面,但它们并没有像您一样提供像您这样的人在定制方面所希望的所有灵活性。他们在Plotly JS Filters 上确实有一个包含基本信息的页面,其中包含一个基本示例,但它实际上不会像您希望的那样从您的图例中删除任何点。

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

此时,我重新研究了 Plotly 文档,并在他们的自定义内置 JS Event Handlers 上找到了一个页面。我开始使用plotly_hover 事件并且我正在取得进展。这个事件的运行频率远低于mousemove,因此它对浏览器来说不那么费力,而且它只在 Plotly JS 事件上运行,所以它实际上只在你需要它的时候运行,这样很方便。但是,我仍然遇到了一些闪烁。

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

还有一些棘手的问题,但我设法把它们都解决了。我为所有图例键(包括具有零值的键)创建了一个包含所有原始 transform 属性值的数组,然后在隐藏零值键后,我遍历那些仍然可见的键并使用自定义重置它们的变换属性正如我提到的 CSS 属性。如果我尝试添加内联 CSS,Plotly 很快就会被覆盖,因此我使用了单独的样式表。

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

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

最后一个变化——我还注意到,即使我的样式大部分都有效,如果你点击 Plotly 画布,Plotly 似乎做了一个快速重置,唯一的解释是如何抵消我的样式那是一个单独的事件,不会触发我的更改。我重新访问了文档并添加了一些其他被触发到事件处理程序列表的侦听器。

为了干净整洁的代码,我将所有处理程序名称放入数组并使用forEach() 循环遍历它们。我支持的事件处理程序是plotly_hoverplotly_clickplotly_legendclickplotly_legenddoubleclick

这是成品:

const testPlot = document.getElementById('test');

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

const addPxToTransform = transform => 'translate(' + transform.replace(/[^\d,.]+/g, '').split(',').map(e => e + 'px').join(',') + ')';

['plotly_hover', 'plotly_click', 'plotly_legendclick', 'plotly_legenddoubleclick'].forEach(trigger => {
  testPlot.on(trigger, function(data) {
    if (document.querySelector('.hoverlayer > .legend text.legendtext')) {
      const legend = document.querySelector('.hoverlayer > .legend');
      legend.style.setProperty('--plotly-legend-transform', addPxToTransform(legend.getAttribute('transform')));
      const legendTexts = Array.from(document.querySelectorAll('.hoverlayer > .legend text.legendtext'));
      const legendTextGroups = Array.from(document.querySelectorAll('.hoverlayer > .legend text.legendtext')).map(text => text.parentNode);
      const transformValues = [];
      legendTexts.filter(label => (transformValues.push(addPxToTransform(label.parentNode.getAttribute('transform'))), label.textContent.includes(' : 0'))).forEach(zeroValue => zeroValue.parentNode.classList.add('zero-value'));
      legendTextGroups.filter(g => !g.classList.contains('zero-value')).forEach((g,i) => g.style.setProperty('--plotly-transform', transformValues[i]));
      const legendBG = document.querySelector('.hoverlayer > .legend > rect.bg');
      legendBG.style.setProperty('--plotly-legend-bg-height', Math.floor(legendBG.nextElementSibling.getBBox().height + 10) + 'px');
    }
  });
});
.hoverlayer > .legend[style*="--plotly-legend-transform"] {
  transform: var(--plotly-legend-transform) !important;
}
.hoverlayer > .legend > rect.bg[style*="--plotly-legend-bg-height"] {
  height: var(--plotly-legend-bg-height) !important;
}
.hoverlayer > .legend [style*="--plotly-transform"] {
  transform: var(--plotly-transform) !important;
}
.hoverlayer > .legend .zero-value {
  display: none !important;
}
<div id="test"></div>
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>

【讨论】:

  • 哇!您对 plotly 内部运作的坚持不懈地挖掘给我留下了深刻的印象,并且可以确认这是有效的。
  • @PattimusPrime 谢谢!这是一个有趣的项目,可以进行和深入研究。感谢您的挑战!
  • 很高兴我不是唯一一个在情节内部摸不着头脑的人。我昨天也走了这么远“这开始起作用了,但是我的风格和 Plotly 似乎坚持使用的风格之间有很多闪烁”,但后来决定离开一天,看看是否有新的想法出现头脑。很高兴看到有人想通了。 Plotly 真的应该让这些东西更具可扩展性。
  • 我想试试你的解决方案,因为它真的很酷(如果可以的话,我会 +5)。我减少了查询选择器的数量并简化了查询选择器,使用了更少的循环,不再需要.parentNode,并使用eventData.points[reverseIdx].y == 0 而不是寻找label.textContent.includes(' : 0')codepen.io/Alexander9111/pen/bGgPrJE
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-02-06
  • 2021-06-20
  • 1970-01-01
  • 1970-01-01
  • 2023-02-04
  • 2023-03-16
  • 2012-01-23
相关资源
最近更新 更多