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

Plotly - Hide data on hover tooltip depending on value?

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

最小示例

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 填充,或者通过配置工具提示以动态隐藏值。

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,其中包含一个基本示例,但它实际上不会像您想要的那样从您的图例中删除任何要点。

接下来,我开始设置 mousemove event listener on the div you assigned the plot to. This started to work, but there was a lot of flickering going on between my styles and those Plotly seemed to persist on using. That brought my attention to the fact that Plotly's scripting was very persistent, which came in handy later. Next, I tried something similar with MutationObserver 因为它应该更快,但是,唉,更多的是相同的。

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

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

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

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

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

计算了高度

最后一个变化——我还注意到,尽管我的风格大部分都有效,但如果你点击 Plotly canvas,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>