两个叠加的热图,使用Plotly.JS设置不透明度。

huangapple go评论65阅读模式
英文:

Two heatmaps on top of each other with opacity with Plotly.JS

问题

我正在尝试将此答案 移植到 100% 的 Plotly.JS 解决方案。
简而言之:如何在 Plotly.JS 中使用两个热图并具有不透明度滑块(无需Python)?

解决方案开始部分,但如何添加第二个数据点?

const z = [];
for (let i = 0; i < 500; i++)
  z.push(Array.from({ length: 600 }, () => Math.floor(Math.random() * 100)));
const data = [{ z: z, colorscale: "YlGnBu", type: "heatmap" }];
const steps = [];
for (let i = 0; i <= 100; i++)
  steps.push({ label: i + "%", execute: true, method: "restyle", args: [{ opacity: i / 100 }] });
const layout = { sliders: [{ name: "slider", steps: steps, active: 100 }] };
Plotly.newPlot("graph", data, layout);

HTML 部分:

<script src="https://cdn.plot.ly/plotly-2.16.2.min.js"></script>
<div id="graph"></div>

请参考原始的 Python 解决方案供参考。

英文:

I'm trying to port this answer to a 100% Plotly.JS solution.
TL;DR : how to have two heatmaps on top of eacher with an opacity slider, with Plotly.JS (no Python)?

Beginning of solution, but how to add the second trace?

<!-- begin snippet: js hide: false console: true babel: false -->

<!-- language: lang-js -->

const z = [];
for (let i = 0; i &lt; 500; i++)
  z.push(Array.from({ length: 600 }, () =&gt; Math.floor(Math.random() * 100)));
const data = [{ z: z, colorscale: &quot;YlGnBu&quot;, type: &quot;heatmap&quot; }];
const steps = [];
for (let i = 0; i &lt;= 100; i++)
  steps.push({ label: i + &quot;%&quot;, execute: true, method: &quot;restyle&quot;, args: [{ opacity: i / 100 }] });
const layout = { sliders: [{ name: &quot;slider&quot;, steps: steps, active: 100 }] };
Plotly.newPlot(&quot;graph&quot;, data, layout);

<!-- language: lang-html -->

&lt;script src=&quot;https://cdn.plot.ly/plotly-2.16.2.min.js&quot;&gt;&lt;/script&gt;
&lt;div id=&quot;graph&quot;&gt;&lt;/div&gt;

<!-- end snippet -->


For reference: original Python solution:

from PIL import Image
import plotly.graph_objects as go
import numpy as np
import scipy.misc

imgA = scipy.misc.face()
imgB = Image.fromarray(np.random.random(imgA.shape[:2])*255).convert(&#39;RGB&#39;)

fig = go.Figure([
    go.Image(name=&#39;raccoon&#39;, z=imgA, opacity=1), # trace 0
    go.Image(name=&#39;noise&#39;, z=imgB, opacity=0.5)  # trace 1
])

slider = {
    &#39;active&#39;: 50,
    &#39;currentvalue&#39;: {&#39;prefix&#39;: &#39;Noise: &#39;},
    &#39;steps&#39;: [{
        &#39;value&#39;: step/100,
        &#39;label&#39;: f&#39;{step}%&#39;,
        &#39;visible&#39;: True,
        &#39;execute&#39;: True,
        &#39;method&#39;: &#39;restyle&#39;,
        &#39;args&#39;: [{&#39;opacity&#39;: step/100}, [1]]     # apply to trace [1] only
    } for step in range(101)]
}

fig.update_layout(sliders=[slider])
fig.show(renderer=&#39;browser&#39;)

答案1

得分: 2

第二个轨迹也进入了data数组。需要注意的是索引很重要:索引为1的轨迹绘制在索引为0的轨迹之上,依此类推。

对于滑块的配置,它应该与Python中的配置相同:每个步骤更改都触发相同的'restyle'方法,带有相同的参数,即Plotly.restyle(graphDiv, ...args),也就是带有args,使方法调用与签名匹配:

Plotly.restyle(graphDiv, update [, traceIndices])

现在,最重要的是滑块应该针对哪个轨迹(traceIndices),也就是哪个索引或哪个明确命名的轨迹的名称(如果我没有记错,默认是全部),但在这里与Python和JavaScript之间没有变化。

这是一个完整的示例(可以在codepen.io上测试):

// 随机生成z数据
const w = {length: 600};
const h = {length: 400};
const z0 = Array.from(h, () => Array.from(w, () => Math.floor(Math.random() * 100)));
const z1 = Array from(h, () => Array.from(w, () => Math.floor(Math.random() * 100)));

// 轨迹“above”的初始不透明度
const op_init = 0.5;

const data = [
  // 注意:轨迹1绘制在轨迹0之上
  {type: 'heatmap', z: z0, colorscale: 'Greys'},                    // 轨迹0
  {type: 'heatmap', z: z1, colorscale: 'Cividis', opacity: op_init} // 轨迹1
];

// 不透明度滑块的步骤
const steps = [];
const n_steps = 100; // 步骤数大于步骤0
for (let i = 0; i <= n_steps; i++) {
  steps.push({
    label: i + '%',
    execute: true,
    method: 'restyle',
    args: [{
      opacity: i/n_steps
    }, [1]] // <- 注意:这仅适用于轨迹1
  });
}

const layout = {
  width: 600,
  sliders: [{
    steps: steps,
    active: Math.round(op_init * n_steps), // 滑块默认与op_init匹配
    pad: {t: 30},
    currentvalue: {prefix: 'opacity: '}
  }]
};

Plotly.newPlot('plot', data, layout);

图像 vs 热图

热图仅适用于单通道数据(根据给定的颜色映射进行单个值到颜色的映射)。

当使用rgb(或rgba,rgba256,hsl,hsla)时,必须使用image类型。区别在于z必须是一个2维数组,其中每个元素都是表示颜色的3个或4个数字的数组(colormodel应相应设置)。

例如,将噪音生成的rgb图像设置为背景层:

const z0 = Array.from(h, () => Array.from(w, () => ['r', 'g', 'b'].map(() => Math.floor(Math.random() * 255)) ));

// ...

const data = [
  {type: 'image', z: z0, colormodel: 'rgb'},                        // 轨迹0
  {type: 'heatmap', z: z1, colorscale: 'Cividis', opacity: op_init} // 轨迹1
];

以下是另一个示例,其中我们有一个rgb[a]图像(DOM对象img)和其像素数据表示为1维Uint8Array(uint8Arr),需要将其转换为2维:

const z0 = [];
const nChannels = uint8Arr.length / img.width / img.height;
const chunkSize = uint8Arr.length / img.height;
const z0_model = nChannels === 4 ? 'rgba' : 'rgb';

for (let i = 0; i < uint8Arr.length; i += chunkSize) {
  const chunk = uint8Arr.slice(i, i + chunkSize);
  const row = [];
  for (let j = 0; j < chunk.length; j += nChannels)
    row.push(chunk.slice(j, j + nChannels));
  z0.push(row);
}

// ...

const data = [
  {type: 'image', z: z0, colormodel: z0_model},                     // 轨迹0
  {type: 'heatmap', z: z1, colorscale: 'Cividis', opacity: op_init} // 轨迹1
];

注意:绘制图像时,y轴会自动翻转(除非另有说明,否则将显示图像倒置)。这会影响热图的y标签的方向,因为它们在同一图上,但仅影响标签而不影响数据。

以下是确保两个轨迹共享相同纵横比且图像定向正确的布局设置:

const layout = {
  // ...
  xaxis: {anchor: 'y', scaleanchor: 'y', constrain: 'domain'},
  yaxis: {anchor: 'x', autorange: 'reversed', constrain: 'domain'},
};
英文:

The second trace goes into the data array as well. The thing to note is that indexing matters : the trace at index 1 is drawn above the trace at index 0, and so on.

For the slider configuration, it should be the same as in python : each step change triggers the same 'restyle' method with the same arguments, ie. Plotly.restyle(graphDiv, ...args), that is, with args such that the method call matches the signature :

Plotly.restyle(graphDiv, update [, traceIndices])

Now, the most important thing is which trace (traceIndices) the slider should target, that is, which index or which name for explicitly named traces (default is all if I'm not wrong), but again here it doesn't change between Python and Javascript.

Here is a full example (play around with it on codepen.io) :

// Random z data
const w = {length: 600};
const h = {length: 400};
const z0 = Array.from(h, () =&gt; Array.from(w, () =&gt; Math.floor(Math.random() * 100)));
const z1 = Array.from(h, () =&gt; Array.from(w, () =&gt; Math.floor(Math.random() * 100)));

// Initial opacity for the trace &#39;above&#39; 
const op_init = 0.5;

const data = [
  // Nb. Trace 1 drawn on top of trace 0
  {type: &#39;heatmap&#39;, z: z0, colorscale: &#39;Greys&#39;},                    // trace 0
  {type: &#39;heatmap&#39;, z: z1, colorscale: &#39;Cividis&#39;, opacity: op_init} // trace 1
];

// Steps for the opacity slider
const steps = [];
const n_steps = 100; // number of steps above step 0
for (let i = 0; i &lt;= n_steps; i++) {
  steps.push({
    label: i + &#39;%&#39;,
    execute: true,
    method: &#39;restyle&#39;,
    args: [{
      opacity: i/n_steps
    }, [1]] // &lt;- Nb. this applies only to trace 1
  });
}

const layout = {
  width: 600,
  sliders: [{
    steps: steps,
    active: Math.round(op_init * n_steps), // slider default matches op_init 
    pad: {t: 30},
    currentvalue: {prefix: &#39;opacity: &#39;}
  }]
};

Plotly.newPlot(&#39;plot&#39;, data, layout);

Image vs Heatmap

A Heatmap works only with single channel data (individual value-to-color mappings according to a given colorscale).

When working with rgb (or rgba, rgba256, hsl, hsla), one has to use the image type. The difference is that z must be a 2-dimensional array in which each element is an array of 3 or 4 numbers representing a color (the colormodel should be set accordingly).

For example, setting an rgb image made of noise as the background layer :

const z0 = Array.from(h, () =&gt; Array.from(w, () =&gt; [&#39;r&#39;, &#39;g&#39;, &#39;b&#39;].map(() =&gt; Math.floor(Math.random() * 255)) ));

// ...

const data = [
  {type: &#39;image&#39;, z: z0, colormodel: &#39;rgb&#39;},                        // trace 0
  {type: &#39;heatmap&#39;, z: z1, colorscale: &#39;Cividis&#39;, opacity: op_init} // trace 1
];

Here a second example where we have an rgb[a] image (DOM object img) and its pixel data represented as a 1-dimensional Uint8Array (uint8Arr), which need to be converted in 2d :

const z0 = [];
const nChannels = uint8Arr.length / img.width / img.height;
const chunkSize = uint8Arr.length / img.height;
const z0_model = nChannels === 4 ? &#39;rgba&#39; : &#39;rgb&#39;;

for (let i = 0; i &lt; uint8Arr.length; i += chunkSize) {
  const chunk = uint8Arr.slice(i, i + chunkSize);
  const row = [];
  for (let j = 0; j &lt; chunk.length; j += nChannels)
    row.push(chunk.slice(j, j + nChannels));
  z0.push(row);
}

// ...

const data = [
  {type: &#39;image&#39;, z: z0, colormodel: z0_model},                     // trace 0
  {type: &#39;heatmap&#39;, z: z1, colorscale: &#39;Cividis&#39;, opacity: op_init} // trace 1
];

Nb. When you plot an image, the yaxis is automatically reversed (unless specified otherwise, which would display the image upside down). This affects the orientation of the heatmap y-labels, as they're on the same plot, but only the labels not the data.

Here is the layout settings ensuring that both traces share the same aspect ratio and that the image is oriented correctly :

const layout = {
  // ...
  xaxis: {anchor: &#39;y&#39;, scaleanchor: &#39;y&#39;, constrain: &#39;domain&#39;},
  yaxis: {anchor: &#39;x&#39;, autorange: &#39;reversed&#39;, constrain: &#39;domain&#39;},
};

huangapple
  • 本文由 发表于 2023年2月6日 18:15:12
  • 转载请务必保留本文链接:https://go.coder-hub.com/75359950.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定